Repository navigation
refactor: port the progress tab to frontend-base - #2247
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## frontend-base #2247 +/- ##
=================================================
+ Coverage 96.53% 96.78% +0.24%
=================================================
Files 433 475 +42
Lines 6841 7368 +527
Branches 1583 1768 +185
=================================================
+ Hits 6604 7131 +527
Misses 223 223
Partials 14 14
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/progress-tab
branch
2 times, most recently
from
November 12, 2026 02:54
3b75d41 to
bf89cde
Compare
11 tasks
3 tasks done
brian-smith-tcril
marked this pull request as ready for review
November 12, 2026 13:18
This was referenced Nov 12, 2026
Base automatically changed from
bsmith/progress-shared-modules
to
frontend-base
November 12, 2026 19:42
course-home/progress-tab moves to the same path under frontend-base/src and both progress routes load it lazily in place of their stubs, in the learning-progress chunk. Platform imports become @openedx/frontend-base ones; LMS_BASE_URL reads become getSiteConfig().lmsBaseUrl, and CREDIT_HELP_LINK_URL and SHOW_UNGRADED_ASSIGNMENT_PROGRESS are read from the app config. The related links and the detailed-grades outline link render the tab URLs the course-tabs hooks resolve, through getLinkProps, so they navigate in the site without a page load. A subsection title links to getSequenceUrl(courseId, blockKey), the page master reached through the jump_to URL of the progress response; that response's url still decides whether the title is a link. The title is a Paragon Hyperlink with the muted and small classes of master's anchor, like the tab's other links. The seven progress slots are placeholders that render their default content until #2185. On master ProgressCertificateStatusSlot passed plugins courseId and ProgressTabCourseCompletionSlot passed enableProgressGraph; the other five passed nothing. Their READMEs and images are dropped with the plugin-framework contract they document. The suite fails on console output, so the warnings the tab raised on master are fixed in the components: the two hover-only overlay triggers drop their trigger for the default of hover and focus, four components lose their defaultProps (two of them build their class list with classNames, so an absent class name is skipped), GradeSummaryTable sets its parent's state in an effect and not while rendering, NoticeRow's message prop type is node, and GradeRangeTooltip's popover has an id. Paragon's own defaultProps warnings from the components this page mounts are silenced by exact pattern under openedx/paragon#3043. LockedDonutSegment drops a viewBox from a g element and one credit message gains a description, both for the package's lint. style.scss loads the donut chart and grade bar partials and takes the three rules of master's index.scss that style this tab: .greyed-out, .locked-overlay and .raised-card. The progress test reaches the page through the route array and asserts the page rules there. The masquerade banner and toolbar request-count cases are skipped until #2194, and the two plugin cases are commented out until #2185. Cases are added for what this commit changes: where the in-page links go, and the credit help link read from the app config. Tests that add coverage follow in their own commit. Part of #2190. Co-Authored-By: Claude <noreply@anthropic.com>
master's progress tests never run 24 lines of the tab's files and leave a branch untaken on about 45 more. The package holds what it ports to tests beside it, so the gaps are filled here, apart from the port, whose commit then holds only what the port changes. In ProgressTab.test.jsx: the Studio link for an administrator; the completion chart's popovers, its all-complete and none-complete forms and a locked segment too thin for its icon; practice scores; each credit eligibility and requirement status; the grade summary's percentages, locked assignment types, footnote names and tooltip grade; the grade range tooltip closing on blur; four further certificate states; and a right-to-left locale. Each of these cases is the only test of some line or branch. utils.test.ts is new, for showUngradedAssignments and getLatestDueDateInFuture. ProgressTabComponents.test.tsx is new and renders one component alone: the complete segment's dividers, and CertificateStatus without course metadata and for a learner who has not passed the entrance exam. Part of #2190. Co-Authored-By: Claude <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/progress-tab
branch
from
November 12, 2026 19:42
bf89cde to
606d5c3
Compare
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
The Progress tab now runs in Learning's frontend-base package. At
/learning/course/:courseId/progressa learner sees their completion, grades and certificate status, and at…/progress/:targetUserIdstaff see another learner's, inside the tab-page frame from #2234. No user-facing change onmaster: the legacy tree undersrc/is untouched.Part of #1905. Part of #2190. Stacked above #2246.
Note
For reviewers: two commits, meant to be read apart. The first is the port, with
master's tests adapted. The second only adds tests, for codemaster's own suite leaves unrun. The ported files are added underfrontend-base/src/as new files, so this diff cannot show what changed frommaster; theport-diffscript in #2234's description prints that diff, fromfrontend-base/manifest.tsv.What changed
course-home/progress-tab/moves to the same path underfrontend-base/src/, and both progress routes load it lazily in place of their stubs, in thelearning-progresschunk. Platform imports become@openedx/frontend-baseones;LMS_BASE_URLreads becomegetSiteConfig().lmsBaseUrl, andCREDIT_HELP_LINK_URLandSHOW_UNGRADED_ASSIGNMENT_PROGRESSare read from the app config (refactor: port the modules the progress tab shares to frontend-base #2246 added the keys).getLinkProps. A subsection title is a ParagonHyperlinktogetSequenceUrl(courseId, blockKey); the progress response'surlstill decides whether the title is a link.masterraises on this page are fixed in the components, since the package's suite fails on console output:trigger="hover"for the default of hover and focus;AssignmentTypeCell,CurrentGradeTooltip,GradeRangeTooltipandPassingGradeTooltiplose theirdefaultProps, and the last two build their class list withclassNamesso an absent class name is skipped;GradeSummaryTablesets its parent's state in an effect, not while rendering;NoticeRow'smessageprop type isnode, andGradeRangeTooltip's popover has anid.defaultPropswarnings from the components this page mounts are silenced in the test setup by exact pattern, under Support for defaultProps will be removed from function components in a future major release. paragon#3043.LockedDonutSegmentdrops aviewBoxfrom a<g>, where it is not a valid attribute; one credit message gains adescription;eslint-disablecomments the package's lint does not know or no longer needs go.style.scssloads the donut chart and grade bar partials and takes the three rules ofmaster'sindex.scssthat style this tab:.greyed-out,.locked-overlayand.raised-card.ProgressTab.test.jsxreaches the page through the exported route array and asserts the page rules there. Its fixtures are completed where the components' prop types require it.hooks.test.jsxports whole. The second commit adds the coverage cases,progress-tab/utils.test.tsandProgressTabComponents.test.tsx.Known differences from
mastermaster's bare anchor hasmuted-link small; theHyperlinkrenders those pluspgn__hyperlink standalone-link.master's cases are kept asit.skipunder that issue.useExamsDatais not exported from the package index. The index exports the app and the routes; the hook is for widgets in the progress slots, so its export is decided with Port slots to frontend-base #2185.Testing
At the layer's head and at the port commit alone, in
frontend-base/: lint, the tests,buildandbuild:cipass. Lint ran with the package's.jsxfiles included (openedx/frontend-base#327).it.skipunder Port the celebrations to frontend-base #2201).Coverage. The package is at 100% of lines at the head, against its 96.15% bar, and at 98.31% at the port commit alone. Counted from the local coverage report the way Codecov counts a patch, this layer adds 470 lines with none missed or partly covered. Codecov's first report on this PR listed one partial line,
CertificateStatus.jsxline 108, whose other branch is taken when a learner has not passed the entrance exam; it has a case now (decision log entry 17).Manual testing on the package's dev site against tutor dev, with one enrolled learner in the demo course: the page and its styles in the shell, the console, the two grade summary tooltips, the dates link, the subsection titles and the outline link, and a staff view of another learner's progress. The record is below.
Decisions
What was decided while building this layer; the scope and the import and config mapping are in #2190, and the role links are decided in #2246.
Full decision log
Decisions: the progress tab (#2247)
Decisions made while building this layer of #2190. The scope and the import and config
mapping are in #2190's plan. The role links, the course-tabs hooks and
getSequenceUrlare decided in #2246, the layer below.
Two commits: the port, then tests that add coverage. The port commit holds the
tab,
master's tests adapted, and new cases only for what the port changes: the pagerules, where the in-page links go, and the credit help link read from the app config.
The second commit holds tests for code
master's own suite leaves unrun (entry 17).Kept apart so the port's diff shows only what the port changes. Both are squashed on
merge.
The in-page links navigate inside the site, and two of them land on placeholders
for now. The related links and the detailed-grades outline link render the URLs the
course-tabs hooks resolve (refactor: port the modules the progress tab shares to frontend-base #2246) through
getLinkProps, so a click is a client-sidenavigation where
masterloads a page. The dates link reaches the ported dates tab.The outline links reach the outline placeholder until Port the outline tab to frontend-base #2191, and the subsection titles
the courseware placeholder until Port the courseware container and redirects to frontend-base #2195.
A subsection title is a Paragon
HyperlinktogetSequenceUrl(courseId, blockKey).masterrenders a bare<a href>to thejump_toURL in the progress response. Theresponse's
urlstill decides whether the title is a link, since the progressserializer withholds it for a subsection a learner may no longer open.
getLinkProps, with an'a'fallback that never ran. Every other navigating linkof the tab is a
Hyperlinkonmaster, so the title is one too:variant="muted"andclassName="small"givemaster'smuted-link smallclasses, beside Paragon's own.
Hyperlinkisdisplay: inline-flex; a long title edited into the page wraps fine(manual pass).
The seven progress slots are placeholders until Port slots to frontend-base #2185. Each keeps its file and
component name and renders its default content with no plugin-framework import.
ProgressCertificateStatusSlotpassed pluginscourseIdandProgressTabCourseCompletionSlotpassedenableProgressGraph; the other five passednothing.
enableProgressGraphstays a prop, since it also gates the chart, and themain-body and side-panel slots keep their window-width gates. Their READMEs and
images get
droppedmanifest lines: they document the plugin-framework contract.Four components lose their
defaultPropsand get no defaults in their place. Reactwarns about
defaultPropson a function component. Default parameters were the firstreplacement, and every caller in the tab passes these props, so each default needed a
standalone test to be reached.
fail.
CurrentGradeTooltiphandsclassName={tooltipClassName}straight on, andAssignmentTypeCellonly testsfootnoteId,footnoteMarkerandlockedfortruthiness, so an absent value behaves as the old default did.
PassingGradeTooltipandGradeRangeTooltipwrote the class name into a templatestring, where an absent one reads
undefined. They build the class list withclassNames, already a dependency, which skips it. The rendered classes aremaster's.The two grade summary overlay triggers drop
trigger="hover". react-bootstrapwarns that a hover-only trigger shuts out keyboard and touch users, and with
master's value nine grade summary cases fail on it. The default of Paragon'sOverlayTriggeris['hover', 'focus'], so the prop goes.IconButtonpopoverin the header with a hover
Tooltipon a bareIcon; its description and reviewgive no reason for hover-only.
Iconcannot take keyboard focus, so the tooltips still open on hover only.Making them keyboard-reachable is a component change, not part of the port.
GradeSummaryTablechanges only where it set its parent's state.mastercallssetAllOfSomeAssignmentTypeIsLocked(true)from a helper that runs while the tablerenders (since [AA-933] fix: address issues with progress page fbe exception UI #561), and React reports "Cannot update a component (
GradeSummary)while rendering a different component (
GradeSummaryTable)"; withmaster'scomponent,
master's own "renders limited feature preview with upgrade button…" casefails on it. The helper keeps its shape: that line sets a local variable, and an
effect after the rows are built passes it to the parent. As on
master, the flag isonly ever set to
true. A version that made the helper pure and rediscovered itsanswer from the rows was a partial cleanup of the file and was not kept.
Two prop types are corrected.
NoticeRow'smessageisnode, notstring:CourseGradeFooterpasses it an element (wrong since refactor: shift grade summary calculation to backend and display "hidden grades" label in the grade table #1797 added the component).GradeRangeTooltip'sPopovergets anid,grade-range-tooltip, which Paragon'sPopoverrequires and the tab's six other popovers have.One more pattern of Paragon warnings is silenced, under Support for defaultProps will be removed from function components in a future major release. paragon#3043. The
tab mounts Paragon components that still set
defaultPropson a function component:Collapsible's parts,DataTable's parts andPopoverContent. Each name was checkedagainst where its
defaultPropsis set: all are in Paragon's own source, so they situnder #3043, and the react-bootstrap pattern under Deprecation warnings from react-bootstrap 1.x components that Paragon wraps paragon#4523 is untouched.
A browser console on the page shows exactly these names.
Lint.
LockedDonutSegmentdropsviewBoxfrom the lock icon's<g>(
react/no-unknown-property: the attribute is only allowed onmarker,pattern,svg,symbolandview). It has been there since the chart's first commit(AA-720: Progress Tab Course Completion chart #407). Rendered with and without it in headless Chrome, the icon is
byte-identical.
credit-information/messages.ts'sminimumGradegains thedescriptionthepackage's lint requires, written for translators.
eslint-disablecomments go where the package's lint does not know the rule(
import/prefer-default-exportinutils.ts, an error) or no longer reports it(four
react/prop-typesones inGradeSummaryTable.jsx, flagged as unused).@stylistic/indent,@stylistic/indent-binary-ops).Styles.
style.scssloads the donut chart and grade bar partials, both portedunchanged, and takes the three rules of
master'ssrc/index.scssthat style thistab and sit in no partial, copied as they are:
.greyed-out,.locked-overlayand.raised-card.The README and the exams hook. The progress README ports with its plugin example
replaced by a line pointing at Port slots to frontend-base #2185, which writes the widget example.
useExamsDataports with its test and is not exported from the package index: the index exports
the app and the routes, and the hook is for widgets in the progress slots, so the
export is decided with Port slots to frontend-base #2185.
master's suite reaches the page through the route array.fetchAndRenderrenders the exported routes at
/learning/course/<id>/progress, waits for the page'slevel-one heading and then for loading to end; a
beforeAllimports the tab'smodule, since the lazy route's first load can outlast a
findBy. Thecourseware-search/hooksmock andlogUnhandledRequestsgo: the first had no readerleft after refactor: port the dates tab to frontend-base #2234, and under the request mock an unanswered request now rejects,
which is the better signal. The two stub cases for the progress paths leave
routes.test.tsx, and their page-rule check runs in the progress test.Fixtures are completed in two places.
SubsectionTitleCellrequiresblockKey,learnerHasAccessandproblemScores, and a prop-type warning fails the suite.block_key(made up, on the first one'spattern) and
learner_has_access: false.false, because twoapi.test.jscasesfor the locked-grades logic rely on it being falsy, as it was when absent.
master's cases write out stay as they are: the test'ssetTabDatahelper gives each ablock_keyand an emptyproblem_scoreswhenthe case leaves them out. Adding the fields to each fixture made 12 one-line hunks
across the file.
A case about a click handler stops the navigation; where a link goes has its own
case. With route links and the real route array, clicking the dates, outline or
subsection link in
master's four "sends event on click" cases navigates, and thecase ends mid-navigation on React's
actwarning. Those cases keepmaster's bodiesplus
preventNavigation(link), which cancels the click's default action; the eventhandler still runs. Destinations are asserted in "Related links and subsection
titles": the hrefs, and one case that follows the dates link to the dates tab.
Cases kept but not run. Five masquerade-banner cases and the "with the instructor
toolbar mounted" request-count case are
it.skipunder Wire up the frontend-base masquerade bar #2194's URL: they need thereal toolbar. The two "exam attempt data for plugins" cases mount a plugin through
the plugin framework and cannot compile without it; they sit in a block comment
under Port slots to frontend-base #2185's URL.
The coverage commit.
master's progress tests never run 24 lines of the tab'sfiles and leave a branch untaken on about 45 more (its coverage report of
2026-10-29). The package holds what it ports to tests beside it.
ProgressTab.test.jsxwas measured by rerunning the suites thatload the tab with that one case left out and comparing coverage. Nineteen are the
only test of some line or branch and are in this commit: the Studio link for an
administrator; the chart's popovers, all-complete and none-complete forms and thin
locked segment; practice scores; the credit eligibility and requirement statuses;
four grade summary cases; the tooltip closing on blur; four certificate states;
two right-to-left cases.
dropped.
utils.test.tsis new, forshowUngradedAssignmentsandgetLatestDueDateInFuture.ProgressTabComponents.test.tsxis new and renders onecomponent alone: the complete segment's dividers, and two
CertificateStatuscases. Without course metadata: that state cannot occur on the page, since the
frame loads the metadata first, but its
?? {}changes the outcome (no card, acertificate_status_disabledevent), so the line stays and gets the case. For alearner who has not passed the entrance exam: nothing on the tab loads that result
in this layer (Progress tab: the certificate card never receives the entrance exam status #2243), so the case mocks the courseware hook; Codecov's first
report on this PR listed that branch of
CertificateStatus.jsxline 108 as thepatch's one partial line.
the nineteen cases alone, the layer's patch, counted as Codecov counts it, was
about 90%.
Coverage at this layer. 100% of lines for the package, and no line of the layer's
patch is missed or partly covered.
The manual pass asks only for what the layer changes and a test cannot see. The
page and its styles in the shell, the console, the two tooltips, the dates link, and
the subsection titles and outline link, with one enrolled learner and no faked
responses. States that need faked data, a config edit or a special course are left
to the suite, and the record names the test for each. The console held nothing from
the tab's own code; one of its third-party warnings had no issue and is now
react-helmetwarns underStrictModefrontend-base#350.Manual testing
What needs eyes is small: the page in the real shell with real data, its styles, the
in-page links navigating inside the site, and a real browser's console. Everything that
depends on particular data (certificate states, credit, locked content, right-to-left) is
left to the suite, listed at the end with where it is tested.
Setup
The same as for the dates tab; nothing new.
bsmith/progress-tab:cd frontend-base && nvm use && npm ci && npm run dev, served athttp://apps.local.openedx.io:2000/learning/, with tutor dev'smfeservice stopped so the port is free.LEARNING_MICROFRONTEND_URLpointing atapps.local.openedx.io:2000/learning, as for Port the dates tab to frontend-base #2189.course-v1:OpenedX+DemoX+DemoCourse.No grades, staff user, faked responses or config edits are needed.
One page, five looks
Open
http://apps.local.openedx.io:2000/learning/course/course-v1:OpenedX+DemoX+DemoCourse/progressas the enrolled learner and stay on it.
with the Progress tab selected. The cards have a soft shadow and no border. The
completion chart is a ring with a percentage in the middle. The grade bar has its
passing-grade marker.
Why here: the route now loads the real page;
style.scssgains the chart andgrade bar partials and the three rules copied from
master'sindex.scss.from Learning's code. One warning is expected and is not Learning's:
findDOMNode is deprecated in StrictMode, withRefHolderand react-bootstrap'sOverlayTriggerin its stack. react-bootstrap raises it for an overlay triggerwhose child is a plain element, here the chart's and grade bar's
<rect>anchors;Deprecated:
findDOMNodeUsed inOverlayTriggerAfter React 18 Upgrade paragon#3425 tracks it, andmaster, which also renders inStrictMode,has the same components.
Why here: the port fixes the warnings
masterraises on this page.in the grade summary table's footer row. Each shows its tooltip.
Why here: both lose their
trigger="hover"for the default.opens without the page reloading (the header does not flash; with Network filtered
to Doc, no new entry). Go back.
Why here: the in-page links are route links now; on
masterthey are full pageloads.
subsection titles read as small muted links, and a long title that wraps still
looks right. Click one: it lands on the placeholder "courseware: not yet ported"
with that subsection's id. Go back. Click "Course outline" in the sentence above the
table: it lands on "outline: not yet ported". Go back.
Why here: the title is a Paragon
Hyperlinkto the sequence's route.Optional
…/progress/<userId>shows "Course progress for ". The suite coversthe heading and the request; this only adds the real route.
Checked outside the app
viewBoxremoved — the icon's<g>, with and without theattribute, was rendered in headless Chrome 157 at the chart's
viewBox: the twoscreenshots are byte-identical, lock icon in place on the segment.
(
ca1189eb), 2026-11-11: all pass. Head: 35 suites, 402 passed, 7 skipped, 100% oflines. Port commit: 33 suites, 364 passed, 7 skipped, 98.31% of lines. One test case
was added to the head afterwards (
bf89cde5: 403 passed), with lint, the suite andbuild:cirerun.Left to the suite
Each of these needs faked responses, a config edit or a special course to see by hand, and
has a test that asserts it:
ProgressTab.test.jsx, "Certificate Status" and "Certificate status card"."Credit requirements".
Grade", "Grade Summary", "Grade summary percentages".
SHOW_UNGRADED_ASSIGNMENT_PROGRESSon and off: "Course Grade",utils.test.ts.ProgressTabComponents.test.tsx.shared/links.test.tsx(refactor: port the modules the progress tab shares to frontend-base #2246). They show only on adownloadable certificate, and the dev site declares neither role.
Not this layer
The frame, toasts, the course bar and the access redirects were checked for #2234. The
entrance exam status is the next layer's section.
Results
2026-11-11, Brian, on the dev site, on
bsmith/progress-tab. The pass ran on thelayer's code as it stands at
bf89cde5, before one test case was added and a commitmessage rewritten; no component changed since.
Brian ticked all five steps and the optional staff view.
Step 5, wrapping: Brian edited a subsection title's text in the page to be long, and it
wrapped fine. The demo course's own titles were not what was tested for this.
Step 2: Brian saved the console of the progress page to a log; it holds 31 distinct
entries, each once. None comes from the progress tab's own code, and none is a warning
the port fixed (no failed prop type, no "Cannot update a component while rendering", no
hover-only trigger warning, no
defaultPropswarning naming a Learning component).By source:
Error with Permissions-Policy header: Unrecognized featurelines, webpack-dev-server's and HMR's start-up lines, the ReactDevTools hint.
userId 4, printed by itsNewRelicLoggingService; React Router'sv7_startTransitionfuture-flag warning (React Router v6 deprecation/future flag warnings frontend-base#325);defaultPropswarnings for
Nav,Navbar,NavbarToggle,NavbarCollapse,NavItemandNavLink, from the shell header'sPrimaryNavLinks.defaultPropswarnings forOverlayTriggerandOverlay(Deprecation warnings from react-bootstrap 1.x components that Paragon wraps paragon#4523) and forPopoverContent,DataTable,DataTableLayout,Table,TableFooter,CollapsibleTrigger,CollapsibleVisibleandCollapsibleBody(Support for defaultProps will be removed from function components in a future major release. paragon#3043), the same names thetest setup silences; three
findDOMNodewarnings from react-bootstrap's overlay andits transition (Deprecated:
findDOMNodeUsed inOverlayTriggerAfter React 18 Upgrade paragon#3425), one of them theRefHolderone withrectinits stack.
Using UNSAFE_componentWillMount in strict mode … SideEffect(NullComponent).Learning renders
HelmetinMain.tsxandtab-page/LoadedTabPage.tsx, both fromearlier layers, and
master'sLoadedTabPagedoes too. Not this layer's; filed asreact-helmetwarns underStrictModefrontend-base#350.🤖 Generated with Claude Code