Repository navigation
fix: handle a slow or failed outline on the courseware and course-end pages - #2139
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2141
September 29, 2026 15:18
…the outline has loaded Adds failing cases for #2138. Both pages gate `TabPage` on the course-home metadata and the courseware metadata only, so when the learning-sequences outline lands after both, they render without it. Each case holds the outline request on a promise it releases later, the test form of the manual repro's delayed outline, and waits for both metadata queries by their query state rather than for the spinner, so the cases hold whether the fix gates the pages on the outline again or makes each reader handle it pending. - `CourseBreadcrumbsSlot/index.test.tsx` (new): the default `CourseBreadcrumbs` inserted as the slot's README shows, through the real FPF (`jest.unmock`, since `setupTest.js` mocks it for every suite), and rendered behind a test gate that waits, as the courseware page does, for both metadata queries. With the outline held the inserted breadcrumbs throw in the `useModels` read; with the outline also in the gate they render (a baseline that passes on this commit). - `CoursewareContainer.test`: the Next control is enabled before it can navigate, a link to the current unit; the page title has an empty segment. - `CourseExit.test`, when the outline is loading: the page does not redirect, and does not render any course-end content. The page's mode depends on `hasScheduledContent`, which only the outline carries, so the two cases run for three learners who are not passing, each of whom gets a different wrong outcome on this commit: an audit learner who cannot view a certificate is redirected, one who can is shown the not-passing page, and a learner with no certificate data who cannot view one is shown the celebration page. Six of these cases fail on this commit (the slot case, the two courseware ones, and one per course-end learner); the next commit on this layer fixes them. Part of #1946. Part of #2138. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utline is loading Since the courseware and course-end pages stopped waiting for the learning-sequences outline, they render in a window where only the metadata has loaded. In that window: - The unit and sequence navigation now renders disabled until it has a link to navigate to, instead of links to the current page. - Course sets the page title only once the outline has loaded, leaving the tab page's title in place rather than one with empty segments. - The course breadcrumbs show their existing not-loaded state instead of throwing inside an inserted plugin. - The course-end page waits for the outline through TabPage, which now accepts several tab-data queries (tabDataQueries) and shows every failed query's error detail, de-duplicated. When the outline fails, the course-end page again redirects to the course home, as it did before the React Query conversion. Closes #2138 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/outline-pending-fixes
branch
from
September 29, 2026 19:29
ebe5c80 to
4800512
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2139 +/- ##
=======================================
Coverage 95.05% 95.06%
=======================================
Files 372 372
Lines 6051 6062 +11
Branches 1486 1493 +7
=======================================
+ Hits 5752 5763 +11
Misses 287 287
Partials 12 12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Sep 30, 2026
Merged
brian-smith-tcril
marked this pull request as ready for review
September 30, 2026 11:24
10 of 19 tasks
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Since #2023 (the course-end page) and #2074 (the courseware page), both pages gate
TabPageon the course-home metadata and the courseware metadata alone, so when the learning-sequences outline lands after both, their children render without it — which the thunk era'sfetchCoursenever allowed. Not every reader copes (#2138): an insertedCourseBreadcrumbsthrows inside its plugin slot, the course-end page shows the wrong mode or redirects away before the outline says the course has scheduled content, the unit navigation arrows render but do nothing, and the page title passes through a state with empty segments. This fixes each: the navigation renders disabled until it has a link,Coursesets the title only once the outline has loaded, the breadcrumbs show their existing not-loaded state, and the course-end page waits for the outline throughTabPage, which now takes a list of tab-data queries. The course-end page also gets back its redirect to the course home when the outline fails, lost in #2023 with the wait. No change once the outline has loaded, and no request change. Part of the Redux → React Query migration (#1946, Stage 1); lands below #2140 (#2089's layer A), which converts these pages'coursewareMetaandsectionsreads. Closes #2138.Two commits, landing as a squash. The first adds the tests that reproduce #2138 and fail against
master; the second fixes them. The PR went up with the test commit alone so CI showed the failures (7) before the fix commit was pushed; the squash keepsmaster's history free of the failing state. The PR title is the squash commit's message, covering both.What changed
UnitNavigation,SequenceNavigationandPreviousButtonderivedisabledfrom the link —!nextLink || (isLastUnit && !exitActive),isFirstUnit || !previousLink— so the controls render disabled untiluseSequenceNavigationMetadatahas links, instead of callingnavigate(undefined)(the current page).Courserenders its<Helmet>only once the outline query has succeeded, leavingLoadedTabPage's "{tab} | {course} | {site}" in place until then.useModels('sections', course.sectionIds ?? []), so the not-loaded state — "Course / / ", the one learners already see while a sequence loads — no longer throws.CourseStatusgainstabDataQueries, mutually exclusive withtabDataQueryin the type;deriveViewtreats the tab as loading while any is pending and as failed if any has failed.CourseExitpasses[metadataQuery, outlineQuery]; the other six callers are unchanged.TabPageshows every failed tab-data query's 403 detail, de-duplicated, one paragraph each, in the error view's existing markup; one detail, or none, renders as before.CourseExitrenders<Navigate to={/course/${courseId}/home} replace>when the outline has failed and the course-home metadata sayshasAccess—outlineFailureRedirect's condition on the courseware page, and the thunk era's behaviour for both.CourseBreadcrumbsSlotsuite through the real FPF;CoursewareContainercases for the Next control and the title;CourseExitcases for three not-passing learners while the outline is loading, and the outline-failure redirect; fourTabPagecases for several tab-data queries;UnitNavigation's renders correctly without units now expects the two disabled buttons.Testing
npm run typesandnpm run lintclean; full suite 118 suites, 1204 passed, 0 skipped. On the test commit alone, 7 cases fail, in CI and locally: the breadcrumbs slot case, the two courseware cases, one per course-end learner, and the outline-failure redirect. Manual checks on tutor local, 11 of 11 run, all passing: the outline delayed 8 s and, separately, failing with a 500, with the course-end learner pinned in the normalizers where a check needs it; see the checklist.Decisions
Full decision log
Decisions — the courseware and course-end pages before the learning-sequences outline has loaded (#2138)
A layer below #2089's layer A, on
master, branchbsmith/outline-pending-fixes: PR#2139 in stack #2141, with #2089's layer A (PR #2140) above it. Two commits: the failing
tests, then the fix (the layer lands as a squash); the PR went up as a draft with the
test commit alone, so CI shows the cases failing before the fix makes them pass. Entries 2
and 3 were tentative until the fix commit was reviewed (2026-09-30), which confirmed them.
Tests first, asserting what the learner sees. Each case holds the outline request
on a promise that never resolves (the manual repro's delayed outline) and waits for
both metadata queries by their query state rather than for the spinner, so it does not
assume whether a page waits for the outline or renders without it — except the
navigation case, which states entry 2's choice. None releases the outline: what the page
shows once it has loaded is the existing tests' (or, for the slot, its baseline case's).
Seven cases fail on
master(the six below and entry 9's outline-failure redirect), eachchecked for the observed reason; the rest pass there. The test commit's message counts
six, omitting the redirect case; it is not rewritten, since the commit's CI run is the
record of the cases failing.
After the query state settles, each case flushes with
actaroundsetTimeout(0)before asserting: TanStack Query 5.102.8's
notifyManagerdelivers observernotifications on
systemSetTimeoutZero, so the components re-render one macrotaskafter the cache's status changes, and an assertion made before then would pass without
the page having rendered.
CourseBreadcrumbsSlot/index.test.tsx(new): the defaultCourseBreadcrumbsinserted as the slot's README shows, through the real FPF (
jest.unmock, sincesetupTest.jsmocks it for every suite), so the plugin's own error boundary containsa failure as it does for an operator. renders inserted breadcrumbs (a baseline with
the outline loaded; passes on
master) comes first, then inserted breadcrumbs do noterror while the outline is loading (fails on
master:CourseBreadcrumbsthrows inuseModels). The slot is the home rather thanCourseBreadcrumbs.test, since thecomponent can be placed anywhere and the slot is where the README inserts it; a first
version in
CoursewareContainer.testmocked the slot to render the component in itsplace, which bypassed FPF and its boundary. The slot renders behind a test gate that
waits, as the courseware page does, for both metadata queries, and by default for the
outline too (
waitForOutline, off in the loading case): rendered bare it threw on itsfirst render, before any query had resolved, so a failure would not have been the
outline's. The gate reads the queries through their own hooks with
{ enabled: false }— a first version observed them by key alone through
useQueries, whose options thenreplaced the query's
meta, so the bridge wrote nothing and the store stayed empty.The plugin config carries a
priority, which FPF requires of an inserted widget(
requiredPluginTypes).CoursewareContainer.test, while the learning-sequences outline is pending:disables the Next control (fails on
master: the control is an enabled link to thecurrent unit, since its
toisundefined) and does not show a page title with anempty segment (fails: "| Demonstration Course | edX"). The suite mocks
Unit, which iswhat renders the unit navigation in the app, so the navigation case sets
mockRenderUnitNavand checks the Link form of Next that the mock renders — the sameuseSequenceNavigationMetadatadata as the top arrows clicked in the manual repro.CourseExit.test, when the outline is loading: does not redirect and does notrender any course-end content, run for three learners who are not passing, each of
whom gets a different wrong outcome on
master(settled in review over cases named foran "in-progress learner": being in progress is what the page can only conclude from the
outline, so it cannot describe the learner before it loads, and over a table-driven
it.eachthat was hard to read). An audit learner (audit_notpassing) who cannot viewa certificate is redirected; one who can is shown
CourseNonPassing; a learner with nocertificate data who cannot view one is shown
CourseCelebration(thetruedefaultsof
isCelebratoryStatus/isEligibleForCertificatewhencertificate_dataisnull). Each of those pages also sendsedx.ui.lms.course_exit.visitedon mount withits variant, so a wrong page records a wrong visit too (from the code; not observed).
The cases render
CourseExitat/course/:courseId/course-endwith a stub page at/course/:courseId, so a redirect shows in the URL, and check the three pages' headingsthrough
messages(congratulationsHeader,endOfCourseHeader,courseInProgressHeader) rather than literal strings, so a reworded heading cannotmake the negative checks pass by default.
The unit navigation renders while the outline is pending, disabled until it can
navigate. Before the React Query conversions nothing below
TabPagerendered until the outline had loaded:
TabPageshowed its spinner until the slice'scourseStatuswasLOADED, whichfetchCourseset only once the outline had beenfetched, and
useSequenceNavigationMetadata's no-link defaults could only be hitwhile the sequence was loading, under
Sequence's own spinner. Since refactor: tear down the courseware Redux slice #2074 the pagerenders before the outline lands, and the buttons are shown enabled with no link, so
they look usable and do nothing. Of the ways to handle that window, a shown-but-dead
control is the observed bug, controls that pop in once the outline lands and a spinner
in their place were both judged worse than controls that are present and disabled from
the first render. That the control navigates once the outline has loaded is the
existing cases' (saves the position on each unit change, and others that click Next).
disabledis derived from the link:disabled = !nextLink || (isLastUnit && !exitActive)inUnitNavigationandSequenceNavigation, anddisabled = isFirstUnit || !previousLinkinPreviousButton. Once the course andsequence have loaded,
nextLinkis always set (the next unit, the next sequence'sfirst unit, or
/course/{courseId}/course-endon the last unit) andpreviousLinkismissing only on the course's first unit, where Previous is already disabled; the
entrance-exam early return (
isFirstUnitandisLastUnitbothtrue, no links)already disables both. So a missing link means only "not loaded yet", and "no link"
is exactly "cannot navigate", with no second signal to keep in step. The alternative,
an explicit flag from the hook (for example
isNavigationLoaded), reads more directlyat the call site but adds a field that must agree with the links. The top-arrow slot's
disabledplugin prop is computed inUnitNavigationbefore it is passed on, so itfollows either way.
Coursesets the page title only once the outline has loaded; until thenLoadedTabPage's title stands. Three things set the title:public/index.html'sstatic "Course | {SITE_NAME}", filled at build time, before React renders;
LoadedTabPage's<Helmet>, "{active tab} | {course} | {SITE_NAME}" from thecourse-home metadata — the title every course-home tab shows — once
TabPagerendersits children; and
Course's<Helmet>, "{sequence} | {section} | {course} |{SITE_NAME}", which
react-helmetlets overrideLoadedTabPage's since it is nestedinside it.
Courserenders in the same pass asLoadedTabPage, so its title took overat once, and with the outline pending it was the partial "| Open edX Demo Course | My
Open edX" (observed; the leading "|" is the section read as
{}with no title).Rendering
Course's<Helmet>only once the outline query has succeeded — the sourceof the sequence and, through the store, the section — leaves
LoadedTabPage's "Course | Open edX Demo Course | My Open edX" in that window, thenthe full courseware title: the component sets the title once, each state is complete,
the interim title is one the app already uses, and its course name is the course-home
metadata's
title— unescaped (Investigate courseware metadata API sending course name HTML-escaped #2137) and loaded beforeTabPagerenders anything.Weighed and not taken: leaving the unknown segments out of
Course's title (a titlethe component would update twice, and after Read sections and coursewareMeta from the courseware queries, not useModel #2089's layer A, which reads the course
name from the outline, just "{SITE_NAME}"). The title case needs no change: the interim
title has no empty segment.
While the outline is pending, the breadcrumbs show their existing not-loaded state:
"Course", then the section and sequence items empty, filling in once it lands.
CourseBreadcrumbsalready renders that state whenever it does not have its data: itslinksare always[chapters, sequentials], empty untilisCourseLoaded && sequenceQuery.isSuccess, and eachBreadcrumbItemfalls back to an empty label, sothe breadcrumbs read "Course / / ". It shows today on every move into a different
sequence, while that sequence's query is pending (
Courserenders the breadcrumbsoutside
Sequence's spinner). The outline window reuses it rather than inventing apattern for one case. Simulated first with an
env.config.jsxplugin that held the realbreadcrumbs back 8 s: an empty slot pops the content below it down when they appear, and
the home link alone did not read as breadcrumbs; a "Course / … / …" placeholder was
weighed and not taken, since the app has no such pattern and the sequence-loading
window already uses the empty items. The three slots keep the same height in every
state. So the fix is only that the not-loaded state no longer throws:
useModels('sections', course.sectionIds ?? []), sincesectionIdsreaches the storeonly from the outline's write; Read sections and coursewareMeta from the courseware queries, not useModel #2089's layer A rewrites that read. Before the RQ work
(
57a2f02b~1, before refactor: convert the courseware metadata fetch to React Query #2023) the call wasuseModels('sections', course.sectionIds)with no fallback, and
useModelsrunsids.map, so the breadcrumbs could not havesurvived a missing
sectionIdseither — they never met one:sectionIdscame onlyfrom
normalizeLearningSequencesData(the outline), merged into the course model byfetchCourse'supdateModelsMap,normalizeMetadatahad none, andCourserenderedonly after
fetchCourseSuccess, which required the outline. So the?? []guards astate new since refactor: tear down the courseware Redux slice #2074 and refactor: convert the courseware metadata fetch to React Query #2023, not a restored one; what it renders is the existing
one, since the
linksmemo builds nothing until the course and sequence are loaded.Precedent for the default: the thunk-era
selectors.jsreadconst { sectionIds = [] } = state.models.coursewareMeta[...].The course-end page waits for the outline through
TabPage, whoseCourseStatustakes a list of tab-data queries. The page's mode cannot be right without the
outline:
getCourseExitModedecides from, in order, the entrance exam (not passed byCourseExitContent, sonull), the exit page being active and the learner signed inand enrolled, the certificate status, then
hasScheduledContent && !userHasPassingGrade→
inProgress,isEligibleForCertificate && !userHasPassingGrade && canImmediatelyViewCertificate→nonPassing, a celebratory status →celebration,and otherwise
disabled; while the outline is pending theinProgressbranch isskipped, and
CourseExitContentrenders<Navigate to={/course/${courseId}}>fordisabled— one-way, so the outline landing later cannot bring the learner back. SoCourseExitgivesTabPagethe courseware metadata and the outline as its tab data,as the route's status did before refactor: convert the courseware metadata fetch to React Query #2023.
CourseStatusgainstabDataQueries, a list, beside the optionaltabDataQuery, andits type makes the two mutually exclusive (a union in which each side sets the other
to
never; most callers are.jsx, so for them that documents rather thanenforces).
TabPagereads onlyisPending,isErroranderrorfrom them;deriveViewdefaultstabDataQueriestotabDataQuery ? [tabDataQuery] : []in itsdestructure of
courseStatusand treats the tab as loading while any is pending andas failed if any has failed. The five callers with one query keep
tabDataQueryunchanged,
DiscussionTabstill passes none, andCourseExitpassestabDataQueries. The contract stays generic —metadataQuerythe access authority,the tab data the tab's content, with the combining rules once, in
deriveView— whichis what fix: restore the 403 detail message on query-converted course-home tabs #2073 held to when it rejected an optional
outlineQueryslot onCourseStatusas courseware routing policy belonging to its container. Thatrejection also rested on outline-pending needing no handling because children
tolerate a not-yet-loaded outline, the assumption this layer corrects; here the
outline is not a slot of its own but part of one page's tab data. Weighed and not
taken: the outline beside the one
tabDataQueryas a special-cased field (fix: restore the 403 detail message on query-converted course-home tabs #2073'srejected shape, making the tab contract a special case for one page); a hand-built
query-shaped object combining the two at
CourseExit(TanStack'suseQueriescombineneeds each query's options object, and these two exist only as hooks);replacing
tabDataQuerywith the list for every caller (five of the seven have onequery, so each would wrap it in brackets); and one prop typed as a query or a list of
queries (a singular name holding a list, or a rename for every caller).
User-facing:
TabPagekeeps the header and footer and showsPageLoadingin place ofLoadedTabPageuntil both have loaded, the view every tab shows on a hard load (thecourse tabs are full page loads); arriving through Next shows no spinner, since both
metadata queries and the outline are cached from the courseware page (and, with entry
2, Next cannot be clicked before the outline has loaded); a refetch never returns a
loaded page to the spinner, since
isPendingis true only until a query first hasdata. The recommendations (
useCourseRecommendations) are not fetched by thecourseware page and still load inside their component. An outline failure is not
TabPage's to show: entry 9 restores the redirect to the course home.TabPageshows every failed tab-data query's error detail, one paragraph each.With more than one tab-data query, keeping only the first failure's detail could hide a
learner-facing explanation behind a failure without one.
getErrorDetailreturns thebackend's
detailonly for a 403 (learner-facing prose) andundefinedotherwise, soderiveViewcollects the details of every failed query, de-duplicated, intoerrorDetails, andrenderErrorrenders each as its own paragraph, or the genericfailure message when there are none; the course-home metadata's error path yields a
one-item list. Diagnosis was never at stake: the app
QueryCache'sonErrorlogs everyfailed query on its own, at
errorunless the query'slogStatusAssays otherwise,before
TabPagesees it. The paragraphs keep the error view's existing look — thecentered 30em paragraph between the header and footer slots, from Refactor containers to share more code #61; the 403 detail
in it from fix: restore the 403 detail message on query-converted course-home tabs #2073. The classes and width move from the
<p>to a wrapping<div>, andeach
<p>keeps Bootstrap's default 1rem bottom margin, now inside the div'spy-5padding instead of outside it, so one detail, or none, spaces as on master and the
same margin separates several. Each is keyed by its text: the details are
de-duplicated and the fallback is a single string, so the text is unique in the list
(airbnb's
react/no-array-index-keyrules out the index). Seen in a manualsimulation (the outline and courseware metadata requests throwing 403s with different
details, on the course-end page, before entry 9 restored the outline-failure
redirect): two stacked centered paragraphs.
No caller can show more than one detail today: on course-end, the one
tabDataQueriescaller, an outline failure with access redirects beforeTabPagerenders (entry 9) and without access
TabPage's denied path wins, so the error viewthere only ever carries the courseware metadata's detail. Kept anyway: the list
exists for the loading wait (entry 6), a list of queries can fail more than once, and
showing one failure while dropping the rest would be the surprising behaviour for
whichever page first passes two that can both fail.
"Every" is every failed tab-data query, never the metadata query's detail beside
them. The metadata query is not a peer error source but the gate in front of the tab
data: since refactor: convert the dates tab to React Query #1987
deriveViewresolves the metadata phase (error → failed, pending →loading,
!hasAccess→ denied) before it reads the tab data at all, mirroring thefetchTabthunk, which denied "regardless of the tabDataResult" — refactor: convert the dates tab to React Query #1987 rejected acombined
metadataQuery.isError || tabDataQuery.isErrorbecause a tab-data failurewhile metadata says
hasAccess: falsemust deny, not fail. On a metadata error thetab-data queries may still be pending, or failing only for want of access, which the
failed metadata can no longer tell; so that path shows the metadata's detail alone.
Once access is settled, the tab-data queries are peers in one phase (on course-end,
the courseware metadata and the outline, neither failure explaining the other), so
all their details show. This keeps fix: restore the 403 detail message on query-converted course-home tabs #2073's precedence — metadata's detail
first, then tab data's — widening only its second step from one query to several.
The fix commit's tests. The seven cases from entry 1 pass. One existing case changes
with entry 2:
UnitNavigation.test's renders correctly without units rendered thenavigation with no sequence and no unit and asserted two links — both links to the
current page, the shown-but-dead state — and now asserts that exactly the Previous and
Next controls render, as disabled buttons.
TabPage.testgains a with severaltab-data queries block: renders the Loaded Tab Page when every tab-data query
resolves, displays loading while any tab-data query is loading, displays the 403
detail of every failed tab-data query and displays a detail shared by several failed
tab-data queries once. Negative check, run: with only the first failed query's detail
kept, exactly the every-detail case fails.
The course-end page redirects to the course home on an outline failure with
access, as it did before refactor: convert the courseware metadata fetch to React Query #2023. Found while simulating entry 7: the courseware page
sent the simulated outline 403 to the course home, the course-end page showed
TabPage's error view. In the thunk era both did the same (checked against57a2f02b~1): the course-end route renderedCourseExitinside<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">,fetchCoursedispatchedfetchCourseDeniedwhen both metadata requests succeeded but the outline failed,even with
hasAccess: true, andTabPage's denied path tookgetAccessDeniedRedirectUrl'sdefaultbranch (noerrorCode,activeTabSlugnotoutline) to/course/{courseId}/home. The courseware page kept it: refactor: tear down the courseware Redux slice #2074 moved itinto
outlineFailureRedirectunchanged. The course-end page lost it in refactor: convert the courseware metadata fetch to React Query #2023, whoseuseCourseExitStatusBridgederived its status from the two metadata queries alone,the same change that lost the outline wait (entry 6); on
mastera failed outlineleaves
hasScheduledContentundefinedas a pending one does, permanently. SoCourseExit, before renderingTabWithTimer, renders<Navigate to={/course/${courseId}/home} replace>whenoutlineQuery.isErrorand thecourse-home metadata says
hasAccess—outlineFailureRedirect's condition. In thepage, not
TabPage: as fix: restore the 403 detail message on query-converted course-home tabs #2073 held, outline routing policy belongs to the componentthat owns it, and
TabPage's contract stays generic. Without access nothing changes:TabPagechecks denied before tab-data errors, so the denied redirect byerrorCodeapplies, as
denieddid in the thunk era. While the course-home metadata is pendinghasAccessisundefined, soTabPageshows loading and the redirect follows once itresolves. Test, in the test commit:
CourseExit.test's when the outline failsredirects to the course home, the outline replying 500 (a failure with access, not
an access error) and a stub
/course/:courseId/homeroute; it fails before the fix(the page stays on
/course-end). Also recorded on The courseware and course-end pages render before the learning-sequences outline has loaded #2138 as item 6.Found along the way, not this layer's
/course/{courseId}/{courseId}: with no sequencesBreadcrumbItem'sbaseUrlfallsback to
defaultContent.id, which its fallback sets to the course id.CourseBreadcrumbsSlotREADME's insert example has nopriority, so FPF logs "theinsert operation config is invalid for widget id: default_breadcrumbs_component" for it
(and renders it anyway).
CoursewareContainer.test's should render the sequence_navigation plugin slotcorrectly asserts only
MockedPluginSlot'sdata-testid, so it tests the mock ratherthan the slot.
Manual testing
Checklist
Manual testing — handle a slow or failed outline on the courseware and course-end pages (#2138)
In-browser verification against a live backend (tutor local).
What changed: while the learning-sequences outline is pending, the unit
navigation renders disabled,
CourseleavesLoadedTabPage's page title inplace, the inserted breadcrumbs show their not-loaded state instead of
throwing, and the course-end page waits for the outline through
TabPage(
tabDataQueries); on an outline failure with access the course-end pageredirects to the course home. No request change intended.
The bugs this layer could introduce. (1) Navigation stuck disabled —
disablednow also depends on the link, so a control whose link neverarrives would stay disabled after the outline lands. (2) A missing title —
if
Course's<Helmet>never rendered, the tab would keep the loading title.(3) Course-end stuck loading, or waiting on the wrong thing — the page now
waits on three queries. (4) A redirect where there should be none — the new
course-end redirect firing without an outline failure, or without access.
Setup
getLearningSequencesOutlinein
src/courseware/data/api.js,await new Promise((resolve) => { setTimeout(resolve, 8000); });.Checks marked delayed need it; the rest run without it.
env.config.jsxinsertingCourseBreadcrumbsdirectly intoorg.openedx.frontend.learning.course_breadcrumbs.v1(RenderWidget: CourseBreadcrumbs,priority: 50), not theDelayedBreadcrumbswrapperused while choosing the pending display, which would mount after the outline
and hide the bug.
the normalizers,
hasScheduledContent: trueon the outline,courseExitPageIsActive: true,userHasPassingGrade: false,certificateData: { certStatus: 'audit_notpassing' }on the coursewaremetadata, and
canViewCertificateon the course-home metadata,falseortrueper check.learning_sequences|courseware/course|course_metadata, Console open.Checks
Courseware page, outline delayed
TypeErrorin the console; once it lands, the section and sequence crumbs.Course-end page, outline delayed (in-progress learner)
canViewCertificate: false(delayed): hard-reload/course/{courseId}/course-end: header, loading spinner and footer, no redirect to/course/{courseId}; once the outline lands,CourseInProgress("More content is coming soon!").canViewCertificate: true(delayed): the same; no flash ofCourseNonPassing("You've reached the end of the course!") beforeCourseInProgress.CourseInProgresswith no spinner (everything is cached).Outline failure
Replace the delay with a failure: at the top of
getLearningSequencesOutline,throw Object.assign(new Error('simulated'), { response: { status: 500 } });./course/{courseId}/course-end: redirected to/course/{courseId}/home./course/{courseId}/home, as onmaster.No delay (regression)
/course/{courseId}/course-end, as onmaster(the no-exit-page case, Next disabled, is covered byUnitNavigation.test, not by this learner).CourseInProgresson a hard reload and through Next.disabled), as onmaster.🤖 Generated with Claude Code