refactor: read courseHomeMeta from the query in the tab page, alerts and course-home tabs - #2109
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2080
September 23, 2026 16:43
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2109 +/- ##
==========================================
+ Coverage 93.95% 94.28% +0.33%
==========================================
Files 367 368 +1
Lines 5939 5951 +12
Branches 1431 1404 -27
==========================================
+ Hits 5580 5611 +31
+ Misses 345 327 -18
+ Partials 14 13 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/course-home-meta-query-reads
branch
2 times, most recently
from
September 23, 2026 16:58
d09ae42 to
f7dfd21
Compare
brian-smith-tcril
marked this pull request as ready for review
September 23, 2026 17:03
Base automatically changed from
bsmith/loaded-tab-page-streak-test-assert
to
master
September 23, 2026 17:04
brian-smith-tcril
force-pushed
the
bsmith/course-home-meta-query-reads
branch
from
September 23, 2026 17:04
f7dfd21 to
2b2c3bb
Compare
…and course-home tabs The course-home metadata has been query-backed since #2010, but its readers still went through the model store the bridge mirrors it into. The 31 reads in the tab page, the alerts and the course-home tabs now take the query result directly, as disabled observers under the page that fetches it, so nothing fetches differently: `useCourseHomeMeta(courseId, { enabled: false }).data ?? {}` where the reader destructures, `.data` alone where the whole object feeds a `useMemo`, and the query already in hand in `TabPage` and `DatesTab`. `useCourseHomeMeta` gains a `CourseHomeMeta` type naming the fields the TypeScript readers need, with an index signature for plugins. `LoadedTabPage` renders only after the owner's query settled, so it narrows on `isSuccess` and throws otherwise, the way the React Query docs describe making `data` defined; `TabPage` renders its header while pending and keeps a `Partial` default. Three tab-URL hooks (`useCourseOutlineUrl`, `useDatesTabUrl`, `useProgressTabUrl`) wrap the B1 accessors so `RelatedLinks`, `DetailedGrades` and `CertificateStatusAlert` stop handling a `tabs` array; `tabs` leaves the certificate alert's payload. Every page that owns the query gains a once-per-load request-count case, so a reader that forgets the option fails that page's suite. The bridge entry stays until B3 (#2086), which takes the remaining 22 reads and the celebration writers. Part of #1946 (Stage 1). Closes #2085. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/course-home-meta-query-reads
branch
from
September 23, 2026 19:54
2b2c3bb to
3364657
Compare
Merged
11 tasks
This was referenced Sep 24, 2026
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 course-home metadata has been query-backed since #2010, but 53 readers still went through the model store the bridge mirrors it into. This moves the 31 tab-page, alert and course-home-tab reads onto
useCourseHomeMetadirectly (layer B2 of the #1977 model-store dissolution; B3, #2086, takes the courseware, shared and widget reads and the bridge entry). No user-facing change and no request-count change: readers subscribe with{ enabled: false }under the page that already fetches the query, and every read keeps its existing shape, so each one's behaviour, including its handling of missing data, is inherited exactly. Three progress/outline components take their tab links from new hooks (useCourseOutlineUrl,useDatesTabUrl,useProgressTabUrl) instead of atabsarray, per the issue's follow-up comment. Part of the Redux → React Query migration (#1946, Stage 1). Closes #2085. Stacked above #2108.What changed
useCourseHomeMetais typed. An exportedCourseHomeMetanames the fields this repo's TypeScript readers need, as required fields, with an index signature so plugins importing the hook are not limited to that list (theOutlineTabDatashape from Read the dates and outline tab data from their queries, not useModel #2083, minus the optionality that endpoint's{}branch forced).TabPage'sCourseStatususes it.TabPageandDatesTabread the query they already hold.useEnrollmentAlertandusePrivateCourseAlertread.datawith no default, since the whole object sits in auseMemodependency array. The rest readuseCourseHomeMeta(courseId, { enabled: false }).data ?? {}.LoadedTabPagenarrows onisSuccess, the shape the React Query docs give for "when isdatadefined": it renders only after the owner's query settled, so it checks the status, throws with the course id if not (theSection.tsxguard from Read the dates and outline tab data from their queries, not useModel #2083), and destructures typed data.TabPagerenders its header while pending, so it keeps the default, typedPartial<CourseHomeMeta>.course-tabs/hooks.tswraps B1's accessors as zero-argument, non-fetching hooks.RelatedLinks,DetailedGradesandCertificateStatusAlertuse them;tabsleaves the alert's payload andpropTypes.useCourseHomeMeta; a suite for the three hooks;LoadedTabPage.test.jsxseeds the query per case and covers the guard's throw; a suite forEnrollmentAlert, whose body had never run in a test;TabPage.test.jsxdrops a store dispatch the prop already covered;CoursewareSearch.test.jsxmocks the hook instead ofuseModel; and every page that owns the query gains requests the course metadata once per load (outline, dates, progress, discussion, live,CoursewareContainer,CourseExit).Testing
npm run typesandnpm run lintclean;course-tabs/hooks.test.tsx6/6,EnrollmentAlert.test.tsx3/3 and the 14 touched suites 323/323; full suite 115 suites, 1163 passed, 3 skipped. Negative check: withuseCourseHomeMetaignoring{ enabled: false }, all seven count cases fail (six read two, the courseware page three) and the three hookfetches nothing on its owncases read one. Manual testing per the checklist below, on tutor dev: 21 of 35 checks run, all passing, covering the request counts on every reachable owner page and the crossing, and every reader on the outline, dates, progress, courseware and course-end pages; 14 checks not run because they need course or account state the environment was not set up for (they are marked not run, not failed), and the live tab skipped as not configured.Decisions
Full decision log
Decisions — read
courseHomeMetafrom the query: tab-page, alerts, and course-home tabs (#2085)Layer B2 of the #1977 model-store dissolution, on top of #2107 (PR #2108).
Entries 1–7 were settled in the plan on issue #2085 (posted 2026-09-23, after
review); the rest landed with the code.
Scope is the issue body plus its comment. The body lists the 31
useModel('courseHomeMeta', courseId)sites; the 2026-09-22 comment addsthe three tab-URL hook façades and
CertificateStatusAlertdroppingtabsfrom its payload. Both are done here. The other 22 sites, the two
celebration writers and the bridge
metaentry are B3 (Read courseHomeMeta from the query: courseware, shared, and widgets #2086).Three read shapes, chosen per site. The body's task list says every
site becomes
useCourseHomeMeta(courseId, { enabled: false }).data ?? {};Read the dates and outline tab data from their queries, not useModel #2083's per-site reasoning gives three:
TabPagereceives the owner's queryas
courseStatus.metadataQuery, andDatesTabfetches it two linesabove its read, so both read
metadataQuery.data ?? {}— whatDatesTabdid for its tab payload in Read the dates and outline tab data from their queries, not useModel #2083. A second observer of thesame key in the same component would be pointless.
useEnrollmentAlertandusePrivateCourseAlertput the wholecourseobject in auseMemodependency array, so they read
.datawithout?? {}(
decisions-2083.mdentry 8, the same call foroutlinein the sametwo hooks).
undefinedis referentially stable;{}per render is not.?? {}(26, andLoadedTabPageby a different route — entry 4):
useModelreturned{}for a missingmodel, so the default keeps every destructure and every no-data branch
as it was (
decisions-2083.mdentry 1).Single-field reads use property access, not a one-field destructure with
a default. Nineteen of the sites read one field — seventeen that always
did (
useAccessExpirationMasqueradeBanner,useActiveEnterpriseAlert,EnrollmentAlert, the three reads incourse-start-alert/hooks.js,useScheduledContentAlert,useCourseEndAlert,DateSummary,CourseDates,CourseTools,ProctoringInfoPanel,StartOrResumeCourseCard,CourseGradeHeader,SubsectionTitleCell,Day,CoursewareSearch) plusRelatedLinksandDetailedGradesoncetabsleft them (entry 6) — and each readsuseCourseHomeMeta(courseId, { enabled: false }).data?.field. A firstpass converted only eight by eye and missed nine written as multi-line
one-field destructures; review caught
EnrollmentAlert, and a scriptedcensus found the rest. The value is
unchanged (
{}.fieldandundefined?.fieldare bothundefined), it isthe repo's single-value rule, and it is the shape Read the dates and outline tab data from their queries, not useModel #2083 used for its one
single-field TypeScript reader (
SequenceDueDate). Settled in review froma "don't paint the TypeScript conversion into a corner" angle: a
destructure with
?? {}does convert — it needs aPartial<CourseHomeMeta>annotation, as
TabPageshows — but that is something the converter hasto know and argue for later, whereas
data?.fieldconverts untouched.The multi-field sites keep the
?? {}destructure; their conversion isthe one-token annotation, recorded here so it is not relitigated.
CourseHomeMetanames its fields as required.useCourseHomeMetawastyped as
{ courseAccess?: { hasAccess: boolean } }, with a commentdeferring the rest to
useModel. It now takes an exportedCourseHomeMetanaming what this repo's TypeScript readers need, with an index signature
so plugins importing the hook are not limited to that list — the
OutlineTabDatashape and comment (decisions-2083.mdentries 9 and 11).Unlike
OutlineTabDatathe named fields are required. The outline type isall-optional because that endpoint's 403 branch returns
{}; the metadataendpoint has no such branch (
getCourseHomeCourseMetadatacamel-cases thewhole serializer), so required is the accurate type, and "not loaded" is
expressed the way React Query expresses it —
dataisundefinedandisSuccessisfalse— rather than by every field being optional.TabPage'sCourseStatus.metadataQuerybecomesUseQueryResult<CourseHomeMeta, RequestError>; every caller alreadypasses the hook's result.
verifiedModeisRecord<string, unknown> | null:LoadedTabPageonly tests it for truthiness and passes it to the untypedStreakCelebrationModal, so its fields wait for B3, which types thatmodal's own reads.
celebrationsnames the two fieldsLoadedTabPagederives from; B3 adds
firstSectionandweeklyGoalwhen it convertstheir readers.
LoadedTabPagenarrows onisSuccessand throws otherwise; the two.tsxreaders do not share a shape.data ?? {}widens toCourseHomeMeta | {}, which TypeScript will not destructure(
decisions-2083.mdentry 10). The React Query docs' answer to "when isdatadefined" is the status discriminant:and a disabled observer over a populated cache is in that state:
LoadedTabPagerenders only afterTabPagehas seen the owner's querysettle, so it keeps the query result, checks
isSuccess, and destructuresmetadataQuery.datatyped asCourseHomeMeta— which also handstabsto
CourseTabsNavigationSlotasTabMetadata[]with no coercion. If thequery is not in success it throws, naming the course. That is our
addition, following
Section.tsx:50-52from Read the dates and outline tab data from their queries, not useModel #2083, and it is not abehaviour change in any reachable state. Production never renders
LoadedTabPageoutside success:TabPagerenders it only when themetadata query is neither pending nor in error (
deriveView,shouldRenderContent), which isisSuccess— including the case of afailed refetch over cached data, where
TabPageshows its error view andunmounts
LoadedTabPage. In the one unreachable state, rendered with nodata at all, the old code already crashed: checked on this layer's base by
rendering it with no
courseHomeMetamodel and the tab nav unmocked —TypeError: Cannot read properties of undefined (reading 'map')inCourseTabLinksList, during the initial render, tearing down the tree.The guard keeps that loudness one level up with a message that names the
component and course. The one wrinkle is
useToggle, whose initial value is needed beforethe guard because hooks cannot follow an early throw, so the streak length
is read once above the hooks with optional chaining, and the destructured
celebrationsbelow the guard feeds the two derived lines unchanged.useToggle(!!…): Paragon'suseToggletakesdefaultIsOn?: booleanandthe value is
number | null | undefined. The toggle's state becomestrueinstead of3; its consumers are!!isStreakCelebrationOpenonthe modal prop (already coerced) and
ProductTours'isStreakCelebrationOpen, which isPropTypes.bool.isRequiredand usedas a bare condition, so the coercion is what the prop already asked for
and removes a PropTypes type warning that fired whenever a streak was
pending.
The first draft used an all-optional type and an annotated destructure
(
const {…}: CourseHomeMeta = data ?? {}) in both files, with a separateguard on
tabs. Rejected in review in favour of the shape the officialdocs describe, cited from the docs rather than from blog posts, which
date. Also rejected:
data!or a cast at the read (decisions-2083.mdentry 13), and optional chaining per field (seven chained reads
rewriting a block the docs' shape leaves intact).
TabPagekeeps the default, typed asPartial<CourseHomeMeta>. Itrenders
HeaderSlotwithorg/number/titlewhile the query is stillpending, so narrowing does not apply and the docs offer nothing for a
default.
{}is assignable toPartial<CourseHomeMeta>, every binding isT | undefined, and the header receivesundefinedwhile pending exactlyas the store's
{}gave it.getAccessDeniedRedirectUrlis JavaScriptand runs only under
isDenied, which implies the query settled.The tab-URL hooks do not fetch, and share one private helper.
course-tabs/hooks.tsexportsuseCourseOutlineUrl,useDatesTabUrlanduseProgressTabUrl, eachgetXUrl(useTabs())whereuseTabsreadsuseParams().courseIdanduseCourseHomeMeta(courseId, { enabled: false }).data?.tabs.The issue comment's sketch fetched; every caller renders under an owner,
so these read like every other reader here and like
useProgressData()after Stop the progress tab data refetching from components under the tab #2103. Two observers on one key are one cache entry, pinned by
serves both contexts from one cache entry. The helper is not exported:
the comment's point was that components stop handling
tabs, and apublic
useTabswould invite exactly that. Consumers here:RelatedLinks(outline + dates),DetailedGrades(outline),CertificateStatusAlert(progress).CertificateStatusAlertcalls thehook at the top of the component — it cannot go inside
renderNotPassingCourseEnded, a nested function — so the link is computedon every render instead of in one of four branches; a
findover ahandful of tabs, and the same URL.
tabsleaves the alert's payload,memo deps and
propTypes.LoadedTabPagekeepstabs: the nav slottakes the array, so
getActiveTabTitle(tabs, activeTabSlug)stays ratherthan a fourth hook for one caller that holds
tabsanyway. The B3 sites(
CourseNonPassing,CourseInProgress,HiddenAfterDue) switch when B3rewrites their destructures;
widgetConfig.jskeepshasDiscussionTab(a widget-lifecycle function, not a component), so the accessors stay
exported.
A request-count case in every owner page's suite. Outline, dates,
progress, discussion, live,
CoursewareContainerandCourseExiteachgained requests the course metadata once per load: wait for
queryClient.isFetching()to be 0, then exactly one GET for the exactcourse_metadataURL — Stop the progress tab data refetching from components under the tab #2103's shape. One representative case wasproposed and rejected in review: the property is per page, since a
forgotten
{ enabled: false }inDay.jsxshows only on the dates page.CourseAccessErrorPageis the one owner without a case: its suite mocksuseCourseHomeMetafor every caller. Five helpers created their clientinline in JSX and now hoist and return it (the change Stop the progress tab data refetching from components under the tab #2103 made to the
progress helper);
LiveTabalready held it.CourseExit's helper alsofetches the metadata directly, outside React Query, to seed the store for
its B3 readers, so it now calls
axiosMock.resetHistory()beforerendering — no test in that suite read the history — and the count is of
what rendering requests. Each case reads one on the unchanged code and
one after. Negative check, all at once: making
useCourseHomeMetaignorethe option (
enabled: !!courseId) fails all seven — six read two, thecourseware page three — and the three
fetches nothing on its owncasesin
course-tabs/hooks.test.tsxread one.The other suites.
apiHooks.test.tsx: stays idle with no request when disabled foruseCourseHomeMeta, mirroring theuseProctoringInfoDatacase. Stop the courseware gate queries refetching from components under the gate #2098relied on this through
useIsCourseLoaded; with 31 sites now dependingon the option it has its own case.
course-tabs/hooks.test.tsx(new): each hook returns its tab's URLfrom a client seeded at
courseHomeQueryKeys.metadata(courseId), andfetches nothing on its own on an unseeded one — the
useIsCourseLoadedreader cases. Accessor logic stays covered by
utils.test.ts.TabPage.test.jsx: the last case built a second store and dispatchedaddModelto feed theuseModelread; the prop already carrieshasAccess: false, so the store, the dispatch and the import go.LoadedTabPage.test.jsx: each case seeds the metadata query on a nestedQueryClientProvider(arenderWithMetadatahelper;renderbuildsits own client with no handle), with the factory output camel-cased —
normalizeCourseHomeCourseMetadataiscamelCaseObjectplusisMasquerading, which the component does not read. Seeding ratherthan mounting an owner because the component throws without data on
its first render (entry 4). The streak case keeps its store seeding:
the real modal (Make the
LoadedTabPagestreak test actually assert: an unconditional mock from #354 and a fixture inert since AA-1018 #2107) still readsuseModel('courseHomeMeta')fororg/username, a B3 site.CoursewareSearch.test.jsx: itsuseModelmock becomes auseCourseHomeMetamock returning{ data: { org } }, the shapeCourseAccessErrorPage.test.jsxalready uses; the suite mocks everyhook the component calls and has no endpoint handlers, so seeding or an
owner would have been the file's only real request path. What it no
longer catches — a forgotten option in
CoursewareSearch.jsx— theseven count cases catch, since
CourseTabsNavigationrenders it onevery owner page.
InstructorToolbar.test.jsxpasses unchanged: its only alert assertionis negative, and the two banner hooks now read an empty query instead
of the seeded store. The positive path is
OutlineTab.test.jsxrenderspage banner on masquerade, under the owner.
each of two files.
LoadedTabPage.test.jsxgained throws when renderedbefore the course metadata has loaded: an unseeded client, and the
AppProvidererror boundary'slogErrorcall asserted throughgetLoggingService()— theSection.test.tsxshape from Read the dates and outline tab data from their queries, not useModel #2083, exceptthat the service is read live because
initializeTestStorereconfiguresit after module scope.
EnrollmentAlert.test.tsxis new: thecomponent's body had never run in the whole suite, because the outline
suite's Enroll now cases render the private-course alert (which borrows
enrollment-alert's messages) anduseEnrollmentAlertneeds a privateoutline no fixture provides. Three cases in the
ActiveEnterpriseAlert.test.jsxshape — learner text with the button, staff text without it, and the
click posting the enrolment and reporting
org_keyfrom the seededquery, which is the value this layer moved. Both files are now fully
covered by lines;
LoadedTabPage's one uncovered branch is thepre-existing discount chain.
CoursewareSearch.jsximports two modules namedapiHooks. Its own./data/apiHooks(search results) and course-home's../data/apiHooks(the metadata hook) sit on adjacent lines. A first scripted pass merged
the new import into the wrong one; the fix is two imports, which is also
what a reader needs to see.
Commit type is
refactor:. Who reads moves; nothing fetchesdifferently; the bridge
metastays until B3, souseModel('courseHomeMeta')keeps working for plugins, and no README documents a B2 file's read.
Not
refactor!:— that is B3's, when the bridge entry goes.The
celebrationswindow between B2 and B3 has no visible effect.LoadedTabPagenow readscelebrationsfrom the query, whilerecordModalClosingand the first-section writers still write the storeuntil B3. After the streak modal closes, the query keeps the fetched
streakLengthToCelebrateuntil the owner next fetches (the next tabnavigation,
staleTime0). Its two uses in the window areuseToggle'sinitial value (no effect after mount) and the prop of a modal that is
now closed. The
firstSectionwrites have no B2 reader.Left as is: the conditional hook call in
course-start-alert/hooks.js.IsStartDateInFutureis capitalised, soreact-hooks/rules-of-hookstreats it as a component and does not flag
isEnrolled && IsStartDateInFuture(…).Swapping
useSelectorforuseQueryinside it changes nothing aboutthat: both are hooks, and the hook count flips only if
isEnrolledflips without a remount, which enrolment does not do
(
enrollment-alert/clickHook.js:28reloads). A hoist of thestartreadinto the two callers is a behaviour-preserving cleanup with its own
review, not a reader conversion.
The
CourseExitcount case renders the celebration body (added afterreview of Read courseHomeMeta from the query: courseware, shared, and widgets #2086). As first written, the case rendered
<CourseExit />with the suite's default metadata, whose
enrollment.is_activeisnull, sogetCourseExitModereturneddisabledand the page was aredirect with no body. The count was right for that load —
CourseExit,TabPage,LoadedTabPageand this layer's readers on that page — but thecourse-end bodies, and so every reader B3 (Read courseHomeMeta from the query: courseware, shared, and widgets #2086) converts under
CourseExit, were never on the page being counted; B3's negative check(a body reader flipped to fetching) still read one. The case now sets an
active enrolment and a downloadable certificate and asserts
Congratulations! before counting, so the celebration body and its
readers are part of the load. Folded into this layer rather than B3
because it corrects the case where it is introduced; nothing B3 does to
the source changes what the case sees.
Manual testing
Checklist
Manual testing — read
courseHomeMetafrom the query: tab-page, alerts, and course-home tabs (#2085)In-browser verification against a live backend (tutor dev).
What changed: 31 readers of the course-home metadata that went through the
model store now read the metadata query directly, as disabled observers under
the page that fetches it (
OutlineTab,DatesTab,ProgressTab,LiveTab,DiscussionTab,CoursewareContainer,CourseExit,CourseAccessErrorPage).Three progress/outline components take their tab links from new hooks
(
useCourseOutlineUrl,useDatesTabUrl,useProgressTabUrl) instead of atabsarray. Nothing fetches differently.The two bugs this layer could introduce. (1) A reader that fetches: a
forgotten
{ enabled: false }shows up only as a secondcourse_metadatarequest on that reader's page. (2) A reader that never sees data: a disabled
observer with no owner above it reads
undefinedforever, so its componentsits in its no-data branch without erroring — a header with no course title,
an alert that never appears, a link that never renders. Everything below is
one of those two checks.
Setup
An ordinary self-paced course with graded subsections, a dates tab, a
progress tab, and a verified mode. Two
accounts: an enrolled learner and a staff user who can masquerade.
For the alert checks, an unenrolled learner (or the staff user, not
enrolled) and a course whose start date is in the future.
Optional, for the certificate-status alert's View grades button: a course
that has ended with the learner not passing on a verified
enrolment.
Verify by hand
Request count — one per page (the #2098 protocol)
Hard reload, wait for the Network tab to go idle, count
/api/course_home/course_metadata/:/course/:id/home): 1LiveTab.test.jsx/course/:id, after completing the course or via adirect visit as staff): 1
click a sequence title, wait for idle: 1
outline): 1 per navigation, none extra
Readers that render on every tab (
TabPage,LoadedTabPage)On each of outline, dates, progress and a courseware unit:
<tab title> | <course title> | <site name>, where the first segment isthe LMS title of the tab that claims the page: "Course" on the outline
(the outline maps to the
coursewaretab), then "Dates", "Progress".On a courseware unit
Course.jsxsets its own title from the breadcrumbtrail, so this check does not apply there
courseware unit)
when the account has author access
search sends
edx.course.home.courseware_search.submitwithorg_keyset (Network tab → the segment/tracking request, or the console with
analytics logging on)
Links built from a tab URL (the new hooks)
render and go to the right pages
message renders (a course with no graded scores yet) and goes to the
outline
certificate-status alert shows View grades and it goes to the
progress tab
Outline tab
non-UTC timezone in account settings; the dates shift)
(otherwise absent, as before)
org_key(not yet released) content
Enroll now button enrolls and reloads
Alerts that depend on
courseHomeMetadirectlystarts on alert on the outline
progress tab of a not-yet-started course
whose audit access has expired, on outline and dates (the banner reads
userTimezonefrom this layer's read and the rest from the tab model)/course/:id/access-deniedfor a learner whoseaccess code is
incorrect_active_enterprise(or confirm the pagerenders its alert list for a denied learner)
Dates tab
before
Progress tab
policy / upgrade links, when shown)
/progress/:otherUserIdshows Course progress forStreak celebration
once on load; closing it does not reopen it on the next tab navigation
Results
Run on tutor dev, 2026-09-23. 21 checks run, all passing. 14 checks
marked not run: each needs course or account state this environment was not
set up for (proctored exams, a weekly-goal save, a course ending within the
alert window or with scheduled content, an unenrolled learner on a private
course, a future start date, masquerading as an expired or not-yet-started
learner, an
incorrect_active_enterpriselearner, verified/certificatestates, a three-day streak). None of them failed; they were not exercised.
The live tab was skipped as not configured. Both request-count checks and
every reader on the outline, dates, progress, courseware and course-end pages
were confirmed, including
org_keyin the resume-course and courseware-searchtracking events.
Not covered
once-per-load count is pinned by
LiveTab.test.jsx, and its B2 readers arethe shared ones (
TabPage,LoadedTabPage, the alerts) every other tabexercises.
separate discussions MFE, so learning's
DiscussionTabpage is notreachable by hand. Its once-per-load count is pinned by
DiscussionTab.test.jsx, and its only B2 readers (TabPage,LoadedTabPage, the alerts) are the ones every other tab exercises.useEnrollmentAlert,useLogistrationAlertoutside the outline tab) — unchanged by this layer.🤖 Generated with Claude Code