Skip to content

Port latest master to frontend-base (incl. Redux-to-React-Query migration)#801

Merged
arbrandes merged 26 commits into
openedx:frontend-basefrom
arbrandes:frontend-base-master-ports
Mar 23, 2026
Merged

Port latest master to frontend-base (incl. Redux-to-React-Query migration)#801
arbrandes merged 26 commits into
openedx:frontend-basefrom
arbrandes:frontend-base-master-ports

Conversation

@arbrandes
Copy link
Copy Markdown
Contributor

@arbrandes arbrandes commented Mar 4, 2026

Description

This does what it says on the tin: the main intent is to port the latest master into frontend-base land. However, since these are ports, fixes and improvements related to the changes were added beyond what's in master, as seen below. These show as separate commits so we can decide whether to backport them later.

Headliners

  • Ports 7 pending master commits to the frontend-base branch: dependency cleanups, translation support, unenrollment improvements, credit purchase URL logic, documentation fixes, and new unenrollment survey toggle variable
  • Ports the Redux to React Query migration from master, adapting it to frontend-base conventions (@src/ imports, @openedx/frontend-base APIs, slots/widgets structure)
  • Fixes a circular dependency in site.config.test.tsx that broke the entire test suite (corresponding PR to docs)
  • Runs tsc-alias only after copying assets to /dist/, otherwise SVG imports are not converted to relative paths
  • Other bug fixes:
    • Add staleTime (5 min) to prevent excessive refetching across 15+ consuming components
    • Remove redundant manual refetch on unenroll (React Query's invalidateQueries handles it)
    • Compute Date.now() per call instead of at module load (was producing stale timestamps)
    • Close modals only after mutation succeeds, not optimistically on submit
  • Improvements to the React Query implementation:
    • Performance: transform course data once in the queryFn instead of per card render
    • Simplification: replace BackedDataProvider with direct React Query cache lookups
    • Disambiguation/clarity: use explicit query key for initializeBase invalidation
    • Better UX: add smart retry that skips 4xx errors but retries server/network errors
    • Right tool for the job: separate mutation keys from query keys
    • Simplification: replace useReducer with useState in context providers
    • Better testing: add normal user happy path test for useInitializeLearnerHome

LLM usage notice

Built with assistance from Claude models (mostly Opus 4.6).

Work Log

Porting master commits

Six pre-existing master commits were cherry-picked onto frontend-base in a prior session, forming the base of the frontend-base-master-ports branch:

Circular dependency fix (site.config.test.tsx)

Running npm run test revealed that every test suite failed with EnvironmentTypes being undefined. Root cause: site.config.test.tsx imported { EnvironmentTypes } from @openedx/frontend-base, creating a circular dependency chain (initialize.jssite.config@openedx/frontend-base → still loading). Fixed by using import type { SiteConfig } (erased at compile time) and the string literal 'test' instead of EnvironmentTypes.TEST.

Redux to React Query migration port

The large migration commit from master was ported and adapted to frontend-base conventions:

  • Replaced @edx/frontend-platform references with @openedx/frontend-base equivalents
  • Fixed bare 'utils' imports to use @src/utils
  • Used useAuthenticatedUser() hook instead of the removed AppContext pattern
  • Moved test files to match widget directory structure (containers/widgets/)
  • Removed orphaned test files for deleted source modules

Fixing 13 failing test suites

After porting the migration, 13 test suites (43 tests) failed. Issues were categorized and fixed:

  1. Orphaned tests (2 suites): Deleted AppWrapper/index.test.tsx (no source file); moved ConfirmEmailBanner/hooks.test.jsx to correct widgets/ path
  2. Dead plugin-framework mock (2 suites): Removed jest.mock('@openedx/frontend-plugin-framework') from CoursesPanel and DashboardLayout tests
  3. @src/ asset resolution (2 suites): Post-processed jest.config.js to prepend @src/-prefixed .svg/.png patterns before the @src path resolver (webpack-merge ordering issue)
  4. Missing <MemoryRouter> (2 suites): Added MemoryRouter wrapper for Slot component's useLocation() dependency in 4 test files
  5. AppContext not in frontend-base (1 suite): Changed CreditBanner/views/hooks.js from AppContext + useContext() to useAuthenticatedUser() hook
  6. Duplicate jest.mock (1 suite): Merged two jest.mock('@openedx/frontend-base') calls in SelectSessionModal/hooks.test.jsx
  7. Bare 'utils' import (1 suite): Fixed 4 files to use '@src/utils'
  8. Read-only const reassignment (1 suite): Rewrote api.test.tsx to provide mock values in jest.mock() factories
  9. Wrong provider in context test (1 suite): Removed useMasquerade/useBackedData checks from context/index.test.tsx (those providers aren't in ContextProviders)

Also fixed DashboardLayout.test.jsx Slot assertion — Slot renders children directly without a wrapper div.

Bug fixes from code review

A holistic code review of the migration identified 14 issues, including by not limited to:

  • staleTime (4ec911e): Added 5-minute staleTime to the main query to prevent redundant refetches when 15+ components mount with the same query
  • Redundant refetch (498f182): Removed manual queryClient.invalidateQueries() + refetch() on unenroll — React Query's mutation onSuccess with invalidateQueries already handles this
  • Stale Date.now() (1126c07): Date.now() was computed once at module load and reused for all API calls, producing stale timestamps. Changed to compute per call
  • Optimistic modal close (2126010): Modals were closed on form submit before knowing if the mutation succeeded. Moved close logic to the mutation's onSuccess callback

Commit-by-commit test verification

Each commit was tested individually by stepping through the history with git checkout. One test failure was found and fixed: the masquerade fallback test used gcTime: 0 in its test QueryClient, which caused pre-seeded cache data to be garbage collected before queryClient.getQueryData() could read it. Fixed by removing gcTime: 0 from that specific test's QueryClient.

@arbrandes arbrandes force-pushed the frontend-base-master-ports branch 2 times, most recently from 1b7091b to 39b38c8 Compare March 5, 2026 00:38
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 5, 2026

Codecov Report

❌ Patch coverage is 95.31250% with 24 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (frontend-base@d63d9b7). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...gets/LearnerDashboardHeader/MasqueradeBar/hooks.js 0.00% 9 Missing and 5 partials ⚠️
...components/CourseCardBanners/CertificateBanner.jsx 82.35% 3 Missing ⚠️
...Card/components/CourseCardBanners/CourseBanner.jsx 71.42% 2 Missing ⚠️
...ners/CourseFilterControls/CourseFilterControls.jsx 88.23% 2 Missing ⚠️
...components/CourseCardBanners/CreditBanner/hooks.js 90.00% 1 Missing ⚠️
...ners/CourseCard/components/CourseCardMenu/hooks.js 94.11% 1 Missing ⚠️
src/containers/CoursesPanel/index.jsx 91.66% 1 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff                @@
##             frontend-base     #801   +/-   ##
================================================
  Coverage                 ?   90.36%           
================================================
  Files                    ?      152           
  Lines                    ?     1287           
  Branches                 ?      276           
================================================
  Hits                     ?     1163           
  Misses                   ?      119           
  Partials                 ?        5           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arbrandes arbrandes force-pushed the frontend-base-master-ports branch 2 times, most recently from f574820 to 55f36bb Compare March 5, 2026 11:55
@arbrandes arbrandes force-pushed the frontend-base-master-ports branch from 55f36bb to 83dd294 Compare March 5, 2026 13:32
@arbrandes arbrandes changed the title feat: port master commits including Redux to React Query migration feat: port master commits (including react query migration) Mar 5, 2026
@arbrandes arbrandes force-pushed the frontend-base-master-ports branch from 83dd294 to 4cb9aa3 Compare March 5, 2026 14:58
@arbrandes arbrandes changed the title feat: port master commits (including react query migration) feat: port master commits (including React Query migration) Mar 5, 2026
@arbrandes arbrandes changed the title feat: port master commits (including React Query migration) Port latest master to frontend-base (incl. Redux-to-React-Query migration) Mar 6, 2026
NoyanAziz and others added 13 commits March 12, 2026 08:45
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
…extra repositories (openedx#752)

Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Use 'test' string literal instead of EnvironmentTypes.TEST and
import only the type to avoid circular dependency when mocking
@openedx/frontend-base itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all Redux files and packages. Add React Query hooks, Context
providers, typed API layer, data transformers, and tests. Migrate all
components including CourseCard, CourseCardBanners, CourseFilterControls,
CoursesPanel, Dashboard, modals, and remaining widgets. Reconcile
masquerade implementations with frontend-base providers.ts architecture.

Co-Authored-By: Jacobo Dominguez <jacobo.dominguez@wgu.edu>
Co-Authored-By: Claude <noreply@anthropic.com>
useInitializeLearnerHome() is consumed by 15+ components, and with
staleTime defaulting to 0, every mount and window focus triggered a
redundant background refetch. Set staleTime to 5 minutes since
dashboard data rarely changes while the user is viewing it.

Co-Authored-By: Claude <noreply@anthropic.com>
The useUnenrollFromCourse mutation already calls invalidateQueries on
success, which triggers a refetch automatically. The manual refetch()
call in closeAndRefresh caused the API to be called twice.

Co-Authored-By: Claude <noreply@anthropic.com>
The fallback enrollment date was frozen when the module first loaded.
If the app stayed open across midnight, courses with null lastEnrolled
would get a stale timestamp.

Co-Authored-By: Claude <noreply@anthropic.com>
Previously, modals closed immediately after firing mutations via
mutate(), giving no feedback on failure. Now the close action is
passed as an onSuccess callback so the modal stays open if the
mutation fails.

Co-Authored-By: Claude <noreply@anthropic.com>
Auto-fixed stylistic issues (indentation, semicolons, brace style,
type→interface, Array<T>→T[]) and manually fixed display-name errors
(named wrapper functions), @ts-ignore@ts-expect-error, and
Function→explicit signature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
arbrandes and others added 12 commits March 12, 2026 08:46
Convert GlobalDataContext.jsx to .tsx with proper interface so
setEmailConfirmation and setPlatformSettings are typed as
Dispatch<SetStateAction<T>> | null instead of literal null.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tsconfig declares "types": ["jest"] but @types/jest was not
installed, so TypeScript treated jest as a namespace rather than a
value.  Adding the matching type definitions lets the webpack
type-checker process test files without errors.

Also bump frontend-base to fix the @src alias in jest config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, each useCourseData(cardId) call independently transformed
the full course list via getTransformedCourseDataList just to find one
item. With N course cards, this ran N times per render cycle.

Now the transformation runs once in the queryFn, stored as
coursesByCardId on the cached data. useCourseData does an O(1) lookup
and CoursesPanel reads directly from the pre-transformed data.

Co-Authored-By: Claude <noreply@anthropic.com>
The masquerade fallback data was maintained in a separate context
provider, duplicating what React Query already caches. Now when
masquerading fails, the normal user's data is read directly from the
query cache via queryClient.getQueryData(). This eliminates
BackedDataProvider and its associated useEffect sync.

Co-Authored-By: Claude <noreply@anthropic.com>
Mutations were invalidating with initialize() which produces a key
with trailing undefined, relying on React Query implementation details
for prefix matching. Added initializeBase() that produces a clean
prefix key ['learner-dashboard', 'initialize'] for unambiguous
invalidation of all initialize queries regardless of masquerade user.

Co-Authored-By: Claude <noreply@anthropic.com>
… errors

Client errors (4xx) won't resolve on retry, so skip them. Server errors
(5xx) and network failures get up to 3 retries with React Query's
default exponential backoff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split learnerDashboardQueryKeys into distinct query and mutation key
factories for clearer semantics. Mutation keys now live in
learnerDashboardMutationKeys while query invalidation still uses
learnerDashboardQueryKeys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MasqueradeProvider and FiltersProvider used useReducer with action
type enums for what are simple value states. Replaced with useState
for less boilerplate while preserving the same public API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies the basic flow: API is called without masquerade user,
data is returned with coursesByCardId transformation applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Port of master PR openedx#738 to frontend-base.

Differences from master:
- Config added to src/app.ts instead of .env files and src/config/index.js
  (frontend-base convention)
- Uses useAppConfig() hook instead of importing configuration object
  directly (idiomatic for code running inside the provider tree)
- Default is false (master defaults to true)

Co-Authored-By: Adolfo R. Brandes <adolfo@opencraft.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s/ dirs

Move the asset copy step before tsc-alias so that .svg and .png files
exist in dist/ when tsc-alias resolves @src path aliases. Also restrict
image file copying to files under assets/ directories, excluding
screenshots in slots/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@arbrandes arbrandes force-pushed the frontend-base-master-ports branch from 6af1c15 to 9734e01 Compare March 12, 2026 11:46
@brian-smith-tcril
Copy link
Copy Markdown
Contributor

brian-smith-tcril commented Mar 18, 2026

Review assisted by @claude (I used claude to talk through changes, record my thoughts, keep track of where I was throughout the review process, and assist in writing this summary)

Reviewed all 25 commits commit-by-commit, comparing each against the master commit(s) it ports. The full review log is in the details block below.

Overall

The migration is solid. Commits 1–7 are clean master ports and frontend-base fixes. Commit 8 (the Redux→React Query rewrite) is the bulk of the change and is largely correct — component-level masquerade migrations are handled consistently, deleted Redux infrastructure is appropriate, and new context providers/hooks/API layer are well-structured. Commits 9–25 are focused fixes and improvements with no master equivalents — most address real bugs or performance issues that also exist on master and are worth porting back.

A few things need attention before merge, noted below.


Follow-ups for this PR

Definitely discuss

  • Commit 19 (62b98f95) — smart retry logic — two questions: (1) Why was retry: false set in the original migration? (2) Does skipping retries on 4xx provide enough benefit over React Query's default (retry: 3 for all errors) to justify the custom logic? Simplest alternative: remove retry and let React Query's default handle it.
  • Commit 17 (3f47d3d7) — BackedDataProvider removal — solid simplification, but two gotchas worth acknowledging: (1) if the React Query cache entry is garbage-collected before masquerade failure, getQueryData() returns undefined silently; staleTime: 5min (commit 9) reduces but doesn't eliminate this risk. (2) the query key in getQueryData() must match exactly or you get undefined silently.

Worth investigating / is this intentional?

  • CourseCardDetails/hooks.js — reverts ||?? (nullish coalescing) introduced in 89559a49 on frontend-base, never on master. Intentional?
  • Dashboard/index.jsx — removes <div id="learnerdashboardroot"><main> wrapper added in 89559a49; not clear why it was added or why it's being removed.
  • Dashboard/index.test.jsx — "courses still loading" test logic changed from isPending: true to hasCourses: false on master in 6ae8180f, reverted in 21cb5186 on frontend-base, now re-applied here. Intentional?
  • SocialShareMenu.test.jsx — removes expects from "is disabled" test added in 21cb5186, never on master. Worth keeping?
  • CourseCard/hooks.test.jsforwards formatMessage from useIntl and passes course title and banner URL from course data tests removed on both master and frontend-base. Presumably covered elsewhere — worth confirming.
  • DashboardLayout.test.jsx — replaces specific slot child assertion with expect(sidebarCol.children.length).toBeGreaterThan(0); weaker than before.
  • react-test-renderer — removed from package.json on frontend-base, kept on master. Intentional?
  • jest.config.js — asset mock narrowed to .png only. Non-blocking, but worth a comment — see frontend-base PR #182 re: asset types needing to be maintained in two places.

Nits

  • UnenrollConfirmModal/hooks/index.test.js (commit 24) — nested beforeEach blocks include mock setup already covered by higher-level mocks; each really only needs useAppConfig.mockReturnValue({ SHOW_UNENROLL_SURVEY: ... }).
  • SocialShareMenu.jsx — adds a bare React import after removing useContext; React isn't needed.
  • CourseBanner.test.jsx — test name initializes data with course number from enrollment, course and course run data feels misaligned with the new React Query logic.
  • src/tracking/trackers/socialShare.js — still uses a relative import path instead of @src alias.
  • ConfirmEmailBanner/hooks.js — switches from named useState import to importing React; not clear why.
  • SelectSessionModal/hooks.test.jsx — missing logging mock that mutationHooks.test.tsx has; worth establishing a consistent approach.
  • Masquerade.test.tsx and SelectSession.test.tsx — unicode strings encoded differently (literal chars on master, escape sequences on frontend-base). Worth deciding on a convention.
  • UnenrollConfirmModal/hooks/index.test.jscalls initializeApp api method test asserts on mockRefreshList; test name should be updated.
  • PendingContent.test.jsx — removes expect(button).toHaveAttribute('aria-disabled', 'true') added in 21cb5186; worth investigating why it was added.
  • UnenrollConfirmModal/hooks/index.test.js — removes calls refreshList and close test added in 21cb5186. Worth keeping?
  • data/context/index.tsxMasqueradeProvider re-exported rather than composed; worth understanding why and whether test coverage is adequate.

Items to port back to master

These bugs/improvements were caught or introduced during the frontend-base port and don't yet exist on master:

  • Commit 22 (d854f8c4) — adds normal user happy path test for useInitializeLearnerHome.
  • Commit 21 (5ded3933) — replaces useReducer with useState in MasqueradeProvider and FiltersProvider; single-value states had no benefit from the reducer pattern.
  • Commit 20 (8c4bcd4d) — splits query and mutation key factories for clearer semantics.
  • Commit 18 (b0ac3b67) — adds initializeBase() as a clean prefix key for mutation invalidation; previous initialize() had trailing undefined and relied on React Query prefix-matching implementation details.
  • Commit 16 (ce3baa97) — moves course data transformation from per-card O(N²) to once in queryFn O(N), with O(1) lookups per card via coursesByCardId.
  • Commit 12 (d0ce95d8) — moves modal close from immediate to onSuccess callback; modals previously closed immediately after firing mutations, giving no feedback on failure.
  • Commit 11 (79fd39f8) — moves Date.now() from module-load time to per-call; fixes stale fallback enrollment date if the app stays open past midnight.
  • Commit 10 (675ba935) — removes redundant manual refetch() after unenroll; invalidateQueries on mutation success already handles it, causing a double API hit.
  • Commit 9 (2234bd4d) — adds staleTime: 5min to useInitializeLearnerHome; without it, every mount and window focus in 15+ consuming components triggers a redundant background refetch.
  • MasqueradeBar/index.jsx — frontend-base has a cleaner implementation with logic extracted into useMasqueradeBarData(). Worth investigating backport.
  • data/services/lms/api.test.tsx — file-level jest.mock for constants (cleaner than beforeEach); also no eslint-disable needed for unused vars.

Pre-existing gaps (not introduced by this PR)

  • MasqueradeBar/hooks.js test coverage — frontend-base keeps a rewritten hooks.js but has no corresponding test file.
  • MasqueradeBar hooks pattern inconsistency — master inlines all hook logic into index.jsx; frontend-base keeps a separate hooks.js. Worth aligning.
  • StrictDict.js console logging — master keeps console.error; frontend-base already has neither. Worth deciding which is correct.

Full review log

PR #801 Review Log

PR: #801
Title: Port latest master to frontend-base (incl. Redux-to-React-Query migration)
Base branch: frontend-base
Head branch: frontend-base-master-ports

Goal

Review each commit in the PR by comparing it against the original master commit(s) it ports,
verifying that the adaptation to @openedx/frontend-base is correct and no unintended changes
were introduced.

Approach

@brian-smith-tcril reviews each commit by comparing master commits to PR commit diffs. Claude assists by looking up file lists, master commit details, and logging findings.


Commit Index

Group A — Pre-migration ports from master (commits 1–6)

# PR Commit SHA Headline Master Commit(s) Status
1 424422ca feat: added a generic creditPurchase Url logic (#675) 90aa652c
2 439bcbdc feat: improve unenrollment process (#704) ff925b06
3 67eb0b23 feat: added the ability for instances to use local translations from extra repositories (#752) c2771507
4 2473745b fix(deps): remove filesize dependency (#767) 75396f1d
5 f8fee455 fix: update react-share to v5 (#795) eef5d3f6
6 c1806e0b fix(docs): use correct image for custom course banner (#796) f1180bff

Group B — Frontend-base specific fix (commit 7)

# PR Commit SHA Headline Master Commit(s) Status
7 df355049 fix: break circular dependency in site.config.test.tsx (frontend-base-only)

Group C — Redux→React Query migration (commit 8)

# PR Commit SHA Headline Master Commit(s) Status
8 3d1ec2bd refactor: migrate from Redux to React Query 0d2eb96c ⚠️

Group D — Bug fixes and style (commits 9–13)

# PR Commit SHA Headline Status
9 2234bd4d fix: add staleTime to prevent excessive refetching
10 675ba935 fix: remove redundant manual refetch on unenroll
11 79fd39f8 fix: compute Date.now() per call instead of at module load
12 d0ce95d8 fix: close modals only after mutation succeeds
13 22754dba style: fix lint errors across migrated files

Group E — React Query improvements and follow-ups (commits 14–25)

# PR Commit SHA Headline Status
14 b1955cff fix: type GlobalDataContext to fix build
15 e3b2a473 fix: add @types/jest so ForkTsCheckerWebpackPlugin sees jest globals
16 ce3baa97 perf: transform course data once in queryFn instead of per card
17 3f47d3d7 refactor: replace BackedDataProvider with React Query cache lookup ⚠️
18 b0ac3b67 fix: use explicit initializeBase key for query invalidation
19 62b98f95 fix: use smart retry that skips 4xx errors but retries server/network errors ⚠️
20 8c4bcd4d refactor: separate mutation keys from query keys
21 5ded3933 refactor: replace useReducer with useState in context providers
22 d854f8c4 test: add normal user happy path test for useInitializeLearnerHome
23 e64fc19d style: fix lint errors in queryHooks test
24 a71bd5a7 feat: Unenroll survey is configurable through environment variable
25 9734e01e fix: run tsc-alias after copying assets and limit asset copy to assets/ dirs

Status legend: 🔲 Not started · 🔍 In progress · ✅ Approved · ⚠️ Needs discussion · ❌ Issues found


Reviews

Commit 1 — 424422ca: feat: added a generic creditPurchase Url logic (#675) ✅

Type: Port of master commit
Master refs: 90aa652c

  • src/data/services/lms/urls.js ✅ (getConfig() from @edx/frontend-platformgetAppConfig(appId) from @openedx/frontend-base; core logic identical)
  • src/data/services/lms/urls.test.js ✅ (identical to master)
  • example.env.config.js ⏭️ (master adds CREDIT_PURCHASE_URL; not used on frontend-base — site config is used instead)
  • src/config/index.js ⏭️ (master adds CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL; not needed on frontend-base for the same reason)

Commit 2 — 439bcbdc: feat: improve unenrollment process (#704) ✅

Type: Port of master commit
Master refs: ff925b06

  • Changes across all 13 files identical to master ✅

Commit 3 — 67eb0b23: feat: added the ability for instances to use local translations from extra repositories (#752) ✅

Type: Port of master commit
Master refs: c2771507

  • Changes identical to master ✅

Commit 4 — 2473745b: fix(deps): remove filesize dependency (#767) ✅

Type: Port of master commit
Master refs: 75396f1d

  • package.json change identical to master ✅
  • package-lock.json differs (expected — regen on frontend-base) ✅

Commit 5 — f8fee455: fix: update react-share to v5 (#795) ✅

Type: Port of master commit
Master refs: eef5d3f6

  • Changes identical to master ✅

Commit 6 — c1806e0b: fix(docs): use correct image for custom course banner (#796) ✅

Type: Port of master commit
Master refs: f1180bff

  • Changes identical to master ✅

Commit 7 — df355049: fix: break circular dependency in site.config.test.tsx ✅

Type: Frontend-base-only fix (no master equivalent)

  • Switches EnvironmentTypes.TEST'test' as SiteConfig['environment'] to avoid circular dep when mocking @openedx/frontend-base itself ✅
  • Fix is acceptable for this PR; comment explains the reasoning clearly ✅
  • ⚠️ See follow-up: worth investigating upstream fix in @openedx/frontend-base

Commit 8 — 3d1ec2bd: refactor: migrate from Redux to React Query ⚠️

Type: Port of master commit
Master refs: 0d2eb96c

The migration is solid. The core Redux-to-React-Query rewrite is faithfully ported — all Redux infrastructure (reducers, selectors, store, request hooks, fakeData) deleted, replaced with React Query hooks, React Context providers, a typed API layer, and data transformers. Component-level changes are largely correct, with the masquerade migration (useContext(MasqueradeUserContext)useIsMasquerading() on frontend-base; reduxHooks.useMasqueradeData()useIsMasquerading() on master) handled consistently across all affected files.

Frontend-base-specific differences are expected: @openedx/frontend-base imports, @src path aliases, src/containers/src/widgets/ reorganization, AppContextSiteContext/useAuthenticatedUser(). Several additions from 21cb5186 (a previous frontend-base-only commit) are removed as part of aligning with master — most of these removals are appropriate, though a few are worth double-checking (see follow-ups). A handful of items need attention before merge, noted below.

Files (master commit 0d2eb96c)

Deleted files

  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.js (+0/-56) ✅
  • src/containers/CourseFilterControls/hooks.js (+0/-60) ✅
  • src/containers/CourseFilterControls/hooks.test.js (+0/-122) ✅
  • src/containers/CoursesPanel/hooks.js (+0/-54) ✅
  • src/containers/CoursesPanel/hooks.test.js (+0/-115) ✅
  • src/containers/EmailSettingsModal/hooks.test.js (+0/-71) ✅
  • src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.js (+0/-77) ✅ (removed on master; never existed on frontend-base)
  • src/containers/MasqueradeBar/hooks.js (+0/-69) ✅ (deleted on master; logic inlined into index.jsx; frontend-base keeps hooks abstraction with same logic in src/widgets/LearnerDashboardHeader/MasqueradeBar/hooks.js — see follow-up)
  • src/containers/MasqueradeBar/hooks.test.js (+0/-112) ✅ (deleted on master; never existed on frontend-base — see follow-up)
  • src/containers/RelatedProgramsModal/hooks.js (+0/-10) ✅
  • src/containers/SelectSessionModal/hooks.test.js (+0/-204) ✅
  • src/containers/UnenrollConfirmModal/hooks/reasons.test.js (+0/-178) ✅
  • src/data/redux/app/index.js (+0/-2) ✅
  • src/data/redux/app/reducer.js (+0/-81) ✅
  • src/data/redux/app/reducer.test.js (+0/-124) ✅
  • src/data/redux/app/selectors/appSelectors.js (+0/-23) ✅
  • src/data/redux/app/selectors/appSelectors.test.js (+0/-28) ✅
  • src/data/redux/app/selectors/courseCard.js (+0/-155) ✅
  • src/data/redux/app/selectors/courseCard.test.js (+0/-398) ✅
  • src/data/redux/app/selectors/currentList.js (+0/-58) ✅
  • src/data/redux/app/selectors/currentList.test.js (+0/-185) ✅
  • src/data/redux/app/selectors/index.js (+0/-13) ✅
  • src/data/redux/app/selectors/simpleSelectors.js (+0/-38) ✅
  • src/data/redux/app/selectors/simpleSelectors.test.js (+0/-75) ✅
  • src/data/redux/hooks/app.js (+0/-107) ✅
  • src/data/redux/hooks/index.js (+0/-2) ✅
  • src/data/redux/hooks/requests.js (+0/-45) ✅
  • src/data/redux/index.js (+0/-37) ✅
  • src/data/redux/requests/index.js (+0/-2) ✅
  • src/data/redux/requests/reducer.js (+0/-54) ✅
  • src/data/redux/requests/reducer.test.js (+0/-62) ✅
  • src/data/redux/requests/selectors.js (+0/-40) ✅
  • src/data/redux/requests/selectors.test.js (+0/-110) ✅
  • src/data/services/lms/api.js (+0/-77) ✅
  • src/data/services/lms/api.test.js (+0/-156) ✅
  • src/data/services/lms/fakeData/courses.js (+0/-828) ✅
  • src/data/services/lms/fakeData/testUtils.js (+0/-40) ✅
  • src/data/store.js (+0/-37) ✅
  • src/data/store.test.js (+0/-68) ✅
  • src/data/utils.js (+0/-19) ✅
  • src/data/utils.test.js (+0/-29) ✅
  • src/hooks/api.js (+0/-105) ✅
  • src/hooks/api.test.js (+0/-275) ✅
  • src/test/app.test.jsx (+0/-279) ✅
  • src/test/inspector.js (+0/-50) ✅
  • src/test/messages.js (+0/-29) ✅
  • src/test/utils.js (+0/-3) ✅

Not applicable on frontend-base

  • .env (+1/-0) ⏭️ (not used on frontend-base — site config instead)
  • .env.development (+1/-0) ⏭️ (not used on frontend-base — site config instead)
  • .env.test (+1/-0) ⏭️ (not used on frontend-base — site config instead)
  • example.env.config.js (+1/-0) ⏭️ (not used on frontend-base — site config instead)
  • src/config/index.js (+1/-0) ⏭️ (not used on frontend-base — site config instead)

Pure removals on master

  • src/containers/CourseCard/hooks.js (+0/-17) ✅ (removals identical to master)
  • src/containers/Dashboard/hooks.js (+0/-7) ✅ (removed things never existed on frontend-base)
  • src/containers/Dashboard/hooks.test.js (+0/-21) ✅ (useInitializeDashboard test never existed on frontend-base; other differences are whitespace or expected frontend-base adaptations)
  • src/index.test.jsx (+0/-1) ⏭️ (file doesn't exist on frontend-base — SPA entry point tests don't apply when app is a package)
  • src/setupTest.jsx (+0/-19) ✅ (removals identical to master)
  • src/utils/StrictDict.js (+0/-1) ✅ (master removes a console.log; frontend-base version already has no console logging — see follow-up)

Added files (new in master commit)

  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.test.tsx (+192/-0) ✅ (master gets authenticated user from test wrapper; frontend-base mocks useAuthenticatedUser directly — expected difference)
  • src/containers/EmailSettingsModal/hooks.test.jsx (+134/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.jsx (+111/-0) ✅ (changes identical except for expected frontend-base differences; PR equivalent at src/widgets/LearnerDashboardHeader/ConfirmEmailBanner/hooks.test.jsx)
  • src/containers/SelectSessionModal/hooks.test.jsx (+294/-0) ✅ (mostly identical; nit — see follow-ups)
  • src/containers/UnenrollConfirmModal/hooks/reasons.test.jsx (+262/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/data/context/BackedData.test.tsx (+360/-0) ✅ (identical)
  • src/data/context/BackedDataProvider.tsx (+65/-0) ✅ (identical)
  • src/data/context/Filters.test.tsx (+772/-0) ✅ (identical)
  • src/data/context/FiltersProvider.tsx (+127/-0) ✅ (identical)
  • src/data/context/Masquerade.test.tsx (+580/-0) ✅ (otherwise identical; unicode encoding nit — see follow-ups)
  • src/data/context/MasqueradeProvider.tsx (+65/-0) ✅ (identical)
  • src/data/context/SelectSession.test.tsx (+664/-0) ✅ (otherwise identical; unicode encoding nit — see follow-ups)
  • src/data/context/SelectSessionProvider.tsx (+85/-0) ✅ (identical)
  • src/data/context/index.test.tsx (+61/-0) ⚠️ (frontend-base wraps with BackedDataProvider but drops the MasqueradeAvailable assertion — see follow-up)
  • src/data/context/index.tsx (+25/-0) ⚠️ (differs from master — see follow-up)
  • src/data/hooks/index.ts (+19/-0) ✅ (identical)
  • src/data/hooks/mutationHooks.test.tsx (+346/-0) ✅ (otherwise identical; logging mock present here — see follow-up)
  • src/data/hooks/mutationHooks.ts (+131/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/data/hooks/queryHooks.test.tsx (+140/-0) ✅ (differences make sense — frontend-base adds GlobalDataContext mocking)
  • src/data/hooks/queryHooks.ts (+38/-0) ✅ (differences make sense from a frontend-base perspective)
  • src/data/hooks/queryKeys.ts (+10/-0) ✅ (changes identical)
  • src/data/services/lms/api.test.tsx (+288/-0) ✅ (mostly identical; minor formatting differences; two improvements on frontend-base worth backporting — see follow-ups)
  • src/data/services/lms/api.ts (+95/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/hooks/useCourseData.test.tsx (+330/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/hooks/useCourseData.ts (+14/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/hooks/useCourseTrackingEvent.test.tsx (+389/-0) ✅ (identical)
  • src/hooks/useCourseTrackingEvent.ts (+14/-0) ✅ (identical)
  • src/hooks/useEntitlementInfo.test.tsx (+534/-0) ✅ (identical)
  • src/hooks/useEntitlementInfo.ts (+33/-0) ✅ (identical)
  • src/hooks/useIsMasquerading.test.tsx (+450/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/hooks/useIsMasquerading.ts (+10/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/utils/dataTransformers.test.ts (+629/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/utils/dataTransformers.ts (+67/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/utils/hooks.test.tsx (+55/-0) ✅ (changes identical except for expected frontend-base differences)

Changed only on frontend-base

  • jest.config.js ✅ (removes coverage ignores for now-deleted dirs; adds @src alias mapper; narrows asset mock to .png only — see follow-ups)
  • src/data/constants/files.js ✅ (one-line @src path alias change)
  • src/data/constants/htmlKeys.js ✅ (one-line @src path alias change)
  • src/data/contexts/MasqueradeUserContext.jsx (+0/-*) ✅ (deleted — replaced by useIsMasquerading() hook)
  • src/data/contexts/MasqueradeUserProvider.jsx (+0/-*) ✅ (deleted — replaced by useIsMasquerading() hook)
  • src/data/services/lms/index.js ✅ (one-line @src path alias change)
  • src/providers.ts ✅ (updated to import/export BackedDataProvider and MasqueradeProvider instead of MasqueradeUserProvider)
  • src/widgets/LearnerDashboardHeader/hooks.js ✅ (one-line @src path alias change)

Tiny changes (≤6 lines)

  • src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx (+3/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/MustRequestContent.jsx (+2/-2) ✅ (both migrate to useIsMasquerading() from hooks; master came from reduxHooks, frontend-base came from useContext(MasqueradeUserContext))
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.jsx (+3/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/RelatedProgramsBadge/hooks.jsx (+3/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CoursesPanel/CourseList/index.jsx (+3/-3) ✅ (changes identical)
  • src/containers/CoursesPanel/NoCoursesView/index.jsx (+3/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/LearnerDashboardHeader/index.jsx (+3/-2) ⏭️ (header is completely reworked on frontend-base; no direct equivalent, changes not applicable)
  • src/containers/UnenrollConfirmModal/components/ConfirmPane.jsx (+3/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/UnenrollConfirmModal/components/FinishedPane.jsx (+3/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/UnenrollConfirmModal/hooks/index.js (+2/-2) ⚠️ (master migrates to useInitializeLearnerHome().refetch; frontend-base still uses queryClient.invalidateQueries — see follow-up)
  • src/plugins/LookingForChallengeWidget/index.jsx (+3/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/tracking/trackers/socialShare.js (+2/-2) ✅ (changes identical except for expected frontend-base differences; nit — see follow-ups)
  • tsconfig.json (+4/-0) ✅ (differences expected — frontend-base extends a different base tsconfig)

Small changes (7–12 lines)

  • package.json (+2/-9) ✅ (redux ecosystem removals consistent; @tanstack/react-query not added explicitly on frontend-base as expected; ⚠️ react-test-renderer removed on frontend-base but not on master — see follow-up)
  • src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.jsx (+4/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/index.jsx (+5/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.jsx (+4/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.jsx (+4/-3) ✅ (both migrate to useIsMasquerading() from hooks; master came from reduxHooks.useMasqueradeData, frontend-base came from useContext(MasqueradeUserContext))
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.jsx (+4/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/RejectedContent.test.jsx (+6/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/components/ProviderLink.test.jsx (+4/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.jsx (+5/-5) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.test.jsx (+5/-7) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/index.jsx (+6/-2) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardMenu/index.jsx (+5/-3) ✅ (both migrate to useIsMasquerading() from hooks; master came from reduxHooks.useMasqueradeData, frontend-base came from useContext(MasqueradeUserContext))
  • src/containers/CourseCard/components/CourseCardTitle.jsx (+5/-4) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/Dashboard/DashboardLayout.test.jsx (+10/-0) ⚠️ (master only changes a hook mock; frontend-base checks children's length instead of asserting on LookingForChallengeWidget — see follow-up)
  • src/containers/EmailSettingsModal/hooks.js (+7/-4) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/LearnerDashboardHeader/ConfirmEmailBanner/hooks.js (+5/-3) ✅ (both migrate to useInitializeLearnerHome() for isNeeded; master came from reduxHooks.useEmailConfirmationData(), frontend-base came from useContext(GlobalDataContext); nit — see follow-ups)
  • src/containers/RelatedProgramsModal/index.jsx (+4/-3) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/UnenrollConfirmModal/components/ConfirmPane.test.jsx (+10/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/UnenrollConfirmModal/components/FinishedPane.test.jsx (+10/-0) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/UnenrollConfirmModal/index.test.jsx (+8/-1) ✅ (changes identical except for expected frontend-base differences)
  • src/hooks/index.js (+8/-4) ✅ (changes identical)
  • src/plugin-slots/WidgetSidebarSlot/index.test.jsx (+4/-6) ✅ (changes identical except for expected frontend-base differences)

Medium changes (13–20 lines)

  • src/containers/AppWrapper/index.test.tsx (+15/-0) ⏭️ (file doesn't exist on frontend-base — AppWrapper is only used in App.jsx, which also doesn't exist on frontend-base)
  • src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.jsx (+13/-5) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/ResumeButton.jsx (+13/-5) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.test.jsx (+10/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CourseBanner.jsx (+12/-7) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.jsx (+11/-4) ✅ (both migrate to useIsMasquerading() from hooks; master came from reduxHooks.useMasqueradeData, frontend-base came from useContext(MasqueradeUserContext))
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/ApprovedContent.test.jsx (+7/-9) ✅ (appropriate given masquerade migration — frontend-base no longer needs renderWithMasquerading())
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/EligibleContent.test.jsx (+5/-13) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/PendingContent.test.jsx (+11/-9) ✅ (mostly identical; nit — see follow-ups)
  • src/containers/CourseCard/components/CourseCardBanners/index.test.jsx (+12/-5) ✅ (changes mostly identical; some formatting differences on frontend-base but functionally the same)
  • src/containers/CourseCard/components/CourseCardImage.jsx (+7/-7) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/RelatedProgramsBadge/hooks.test.js (+8/-8) ✅ (core hook migration identical; frontend-base additionally removes redux-only code not present on master — outer beforeEach mock and mockReset() calls — which makes sense given the migration; also catches up letconst from master)
  • src/containers/CourseCard/components/hooks.js (+10/-7) ✅ (both migrate to useIsMasquerading() from hooks; master came from reduxHooks.useMasqueradeData, frontend-base came from useContext(MasqueradeUserContext); otherwise identical except for expected frontend-base differences)
  • src/containers/CourseFilterControls/ActiveCourseFilters.jsx (+5/-12) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CoursesPanel/NoCoursesView/index.test.jsx (+8/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/Dashboard/index.jsx (+10/-9) ✅ (logic identical; frontend-base additionally removes <div id="learnerdashboardroot"><main> wrapper — see follow-up)
  • src/containers/LearnerDashboardHeader/index.test.jsx (+8/-6) ⏭️ (no equivalent on frontend-base — header is completely reworked)
  • src/containers/UnenrollConfirmModal/hooks/index.test.js (+7/-8) ⚠️ (core migration identical; several frontend-base-only additions from 21cb5186 removed — see follow-ups)
  • src/containers/UnenrollConfirmModal/hooks/reasons.js (+12/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/plugins/LookingForChallengeWidget/index.test.jsx (+8/-6) ✅ (changes identical except for expected frontend-base differences)

L changes (21–50 lines)

  • src/App.jsx (+9/-39) ⏭️ (file doesn't exist on frontend-base — not an SPA)
  • src/App.test.jsx (+21/-24) ⏭️ (file doesn't exist on frontend-base — not an SPA)
  • src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx (+25/-23) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx (+29/-21) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx (+14/-8) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardActions/index.test.jsx (+13/-22) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx (+30/-11) ✅ (changes identical except for expected frontend-base differences and minor formatting)
  • src/containers/CourseCard/components/CourseCardBanners/CourseBanner.test.jsx (+20/-14) ✅ (changes identical except for expected frontend-base differences; nit — see follow-ups)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.js (+26/-4) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/hooks.test.js (+17/-9) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/MustRequestContent.test.jsx (+10/-11) ✅ (both migrate to mocking useIsMasquerading(); master came from reduxHooks.useMasqueradeData, frontend-base came from MasqueradeUserContext.Provider)
  • src/containers/CourseCard/components/CourseCardBanners/CreditBanner/views/hooks.js (+14/-7) ✅ (expected path diffs; username sourced from AppContext on master, useAuthenticatedUser() on frontend-base — expected difference)
  • src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.jsx (+15/-7) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardDetails/hooks.js (+11/-10) ✅ (mostly identical; ??|| revert — see follow-ups)
  • src/containers/CourseCard/components/CourseCardMenu/SocialShareMenu.jsx (+11/-10) ✅ (both migrate to useIsMasquerading(); master came from reduxHooks.useMasqueradeData, frontend-base came from useContext(MasqueradeUserContext); nit — see follow-ups)
  • src/containers/CourseCard/components/CourseCardMenu/SocialShareMenu.test.jsx (+20/-22) ⚠️ (masquerade migrated from reduxHooks.useMasqueradeData on master to MasqueradeUserContext.Provider on frontend-base; frontend-base also removes expects from the "is disabled" test that were added in 21cb5186 and never on master — see follow-ups)
  • src/containers/CourseCard/components/CourseCardMenu/hooks.js (+26/-5) ✅ (changes identical except for expected frontend-base differences and minor formatting)
  • src/containers/CourseCard/components/CourseCardMenu/index.test.jsx (+13/-14) ✅ (both migrate masquerade; master came from reduxHooks.useMasqueradeData, frontend-base came from MasqueradeUserContext.Provider; otherwise identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardTitle.test.jsx (+10/-12) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseFilterControls/ActiveCourseFilters.test.jsx (+32/-6) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CoursesPanel/index.jsx (+39/-7) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/Dashboard/index.test.jsx (+11/-10) ⚠️ (useInitializeApp mock replaced with useInitializeLearnerHome; "courses still loading" test logic change — see follow-ups)
  • src/containers/MasqueradeBar/index.jsx (+30/-15) ✅ (frontend-base has a cleaner implementation than master — logic delegated to useMasqueradeBarData() hook; the fix ported correctly; see follow-ups)
  • src/containers/RelatedProgramsModal/index.test.jsx (+12/-9) ✅ (identical)
  • src/containers/SelectSessionModal/hooks.js (+26/-13) ✅ (changes identical except for expected frontend-base differences)
  • src/index.jsx (+19/-6) ✅ (closest equivalent on frontend-base is Main.jsx; both migrate away from Redux — master removes the store from AppProvider, frontend-base uses ContextProviders; expected difference)

XL changes (51–100 lines)

  • src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx (+52/-17) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardBanners/EntitlementBanner.test.jsx (+68/-15) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardDetails/hooks.test.js (+27/-27) ✅ (changes ported from master are improvements; minor frontend-base difference: "forwards provider name" test no longer needs to explicitly add providerName since providerData already includes it)
  • src/containers/CourseCard/components/CourseCardImage.test.jsx (+38/-17) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CourseCard/components/CourseCardMenu/hooks.test.js (+36/-29) ✅ (changes identical except for expected frontend-base differences and minor formatting)
  • src/containers/CourseCard/components/hooks.test.js (+38/-23) ✅ (masquerade migrated from MasqueradeUserContext.Provider wrapper to useIsMasquerading() mock, removing related wrapper boilerplate; otherwise identical except for expected frontend-base differences)
  • src/containers/CourseCard/hooks.test.js (+23/-49) ✅ (changes identical except for expected frontend-base differences; both master and frontend-base remove forwards formatMessage from useIntl and passes course title and banner URL from course data tests — see follow-ups)
  • src/containers/CourseFilterControls/CourseFilterControls.jsx (+34/-33) ✅ (changes identical except for expected frontend-base differences)

XXL changes (100+ lines)

  • package-lock.json (+33/-151) ⏭️ (regenerated on both sides — differences expected)
  • src/containers/CourseFilterControls/CourseFilterControls.test.jsx (+116/-41) ✅ (changes identical except for expected frontend-base differences)
  • src/containers/CoursesPanel/index.test.jsx (+103/-35) ✅ (changes identical except for expected frontend-base differences and minor formatting)
  • src/containers/MasqueradeBar/index.test.jsx (+120/-47) ⏭️ (no equivalent on frontend-base)
  • src/data/constants/app.test.js (+163/-0) ✅ (changes identical)

Commit 25 — 9734e01e: fix: run tsc-alias after copying assets and limit asset copy to assets/ dirs ✅

Type: Frontend-base-only (no master equivalent)

  • Makefile (+2/-2) — reorders asset copy before tsc-alias so .svg and .png exist in dist/ when path aliases are resolved; restricts copy to assets/ dirs, excluding slots/ screenshots. Related to frontend-base PR #182 and the jest.config.js asset mock narrowing in commit 8.

Commit 24 — a71bd5a7: feat: Unenroll survey is configurable through environment variable ✅

Type: Port of master PR #738
Master refs: ca954e13

  • src/app.ts (+1/-0) ✅ — adds SHOW_UNENROLL_SURVEY to site config (master uses .env files and src/config/index.js)
  • src/containers/UnenrollConfirmModal/hooks/index.js ✅ — uses useAppConfig() hook instead of direct config import; changes make sense
  • src/containers/UnenrollConfirmModal/hooks/index.test.js ✅ — port correct; nit — see follow-ups

Commit 23 — e64fc19d: style: fix lint errors in queryHooks test ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryHooks.test.tsx (+12/-4) — lint fixes only

Commit 22 — d854f8c4: test: add normal user happy path test for useInitializeLearnerHome ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryHooks.test.tsx (+28/-0) — adds happy path test for the normal (non-masquerade) flow verifying API call and coursesByCardId transformation

Worth porting to master.


Commit 21 — 5ded3933: refactor: replace useReducer with useState in context providers ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/context/MasqueradeProvider.tsx — single string | undefined state; useReducer with one action type had no real discriminated union benefit; useState is equally type-safe with the same public API
  • src/data/context/FiltersProvider.tsx — same pattern

No type safety lost; public API unchanged. Worth porting to master.


Commit 20 — 8c4bcd4d: refactor: separate mutation keys from query keys ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryKeys.ts — splits into learnerDashboardQueryKeys and learnerDashboardMutationKeys
  • src/data/hooks/mutationHooks.ts — updated to use mutation keys

Worth porting to master.


Commit 19 — 62b98f95: fix: use smart retry that skips 4xx errors but retries server/network errors ⚠️

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryHooks.ts — changes retry: false to a custom function that skips retries on 4xx but allows up to 3 retries on 5xx/network errors
  • src/data/hooks/queryHooks.test.tsx — adds tests for retry behavior

See follow-ups for questions before approving.


Commit 18 — b0ac3b67: fix: use explicit initializeBase key for query invalidation ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryKeys.ts (+1/-0) — adds initializeBase() returning ['learner-dashboard', 'initialize'] as a clean prefix key
  • src/data/hooks/mutationHooks.ts — mutations now invalidate with initializeBase() instead of initialize() (which had trailing undefined, relying on React Query prefix matching implementation details)

Worth porting to master.


Commit 17 — 3f47d3d7: refactor: replace BackedDataProvider with React Query cache lookup ⚠️

Type: Frontend-base-only (no master equivalent)

  • Deletes BackedDataProvider.tsx and BackedData.test.tsx entirely
  • queryHooks.ts — masquerade fallback now reads directly from React Query cache via queryClient.getQueryData()
  • providers.tsBackedDataProvider removed from provider tree
  • index.tsx / index.test.tsxBackedDataProvider removed from context composition

Solid simplification — eliminates a manual duplication of what React Query already provides. See follow-ups for cache eviction and key-matching gotchas.


Commit 16 — ce3baa97: perf: transform course data once in queryFn instead of per card ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/hooks/queryHooks.ts — transformation now runs once in queryFn, storing coursesByCardId on the cached data
  • src/hooks/useCourseData.ts — simplified to an O(1) lookup into coursesByCardId
  • src/hooks/useCourseData.test.tsx — memoization and integration test suites removed (no longer relevant); remaining tests updated to use coursesByCardId
  • src/containers/CoursesPanel/index.jsx / index.test.jsx — reads pre-transformed data directly

Worth porting to master.


Commit 15 — e3b2a473: fix: add @types/jest so ForkTsCheckerWebpackPlugin sees jest globals ✅

Type: Frontend-base-only (no master equivalent)

  • package.json (+2/-1) ✅ — adds @types/jest; bumps frontend-base to fix @src alias in jest config
  • package-lock.json — expected churn from version bump

Commit 14 — b1955cff: fix: type GlobalDataContext to fix build ✅

Type: Frontend-base-only (no master equivalent)

  • src/data/contexts/GlobalDataContext.jsxGlobalDataContext.tsx — converts to TypeScript; adds GlobalDataContextType interface with setEmailConfirmation and setPlatformSettings typed as Dispatch<SetStateAction<T>> | null (the .jsx version had no type annotation, so TypeScript inferred the setters as null rather than the union, breaking the build)

Commit 13 — 22754dba: style: fix lint errors across migrated files ✅

Type: Frontend-base-only (no master equivalent)

  • 21 files, all mechanical lint fixes: indentation, semicolons, brace style, type→interface (simple object shapes only — functionally identical, enforced by consistent-type-definitions), Array<T>→T[], @ts-ignore→@ts-expect-error, named wrapper functions for display-name errors, explicit function signatures. No logic changes.

Commit 12 — d0ce95d8: fix: close modals only after mutation succeeds ✅

Type: Frontend-base-only fix (no master equivalent)

  • src/containers/EmailSettingsModal/hooks.js (+4/-2) ✅ — moves close action from immediate mutate() call to onSuccess callback
  • src/containers/SelectSessionModal/hooks.js (+4/-7) ✅ — same pattern

Worth porting to master.


Commit 11 — 79fd39f8: fix: compute Date.now() per call instead of at module load ✅

Type: Frontend-base-only fix (no master equivalent)

  • src/utils/dataTransformers.ts (+5/-3) ✅ — moves Date.now() from module-load time into the function body; fixes a bug where courses with null lastEnrolled would get a frozen fallback timestamp if the app stayed open past midnight

Worth porting to master.


Commit 10 — 675ba935: fix: remove redundant manual refetch on unenroll ✅

Type: Frontend-base-only fix (no master equivalent)

  • src/containers/UnenrollConfirmModal/hooks/index.js (+1/-8) ✅ — removes manual refetch() call from closeAndRefresh; invalidateQueries on mutation success already triggers a refetch, so the manual call was causing a double API hit
  • src/containers/UnenrollConfirmModal/hooks/index.test.js (+0/-12) ✅ — removes tests for the now-deleted manual refetch behavior

Worth porting to master.


Commit 9 — 2234bd4d: fix: add staleTime to prevent excessive refetching ✅

Type: Frontend-base-only fix (no master equivalent)

  • src/data/hooks/queryHooks.ts (+1/-0) ✅ — adds staleTime: 5 * 60 * 1000 (5 min) to useInitializeLearnerHome; fix is correct and worth porting to master

Follow-ups

Worth porting to master

  • Commit 22 (d854f8c4) — adds normal user happy path test for useInitializeLearnerHome.

  • Commit 21 (5ded3933) — replaces useReducer with useState in MasqueradeProvider and FiltersProvider; single-value states had no benefit from the reducer pattern.

  • Commit 20 (8c4bcd4d) — splits learnerDashboardQueryKeys into separate query and mutation key factories for clearer semantics.

  • Commit 18 (b0ac3b67) — adds initializeBase() as a clean prefix key for mutation invalidation; replaces reliance on initialize() with trailing undefined and React Query prefix-matching implementation details.

  • Commit 16 (ce3baa97) — moves course data transformation from per-card O(N²) to once in queryFn O(N), with O(1) lookups per card via coursesByCardId.

  • Commit 12 (d0ce95d8) — moves modal close from immediate to onSuccess callback in EmailSettingsModal and SelectSessionModal; modals previously closed immediately after firing mutations, giving no feedback on failure.

  • Commit 11 (79fd39f8) — moves Date.now() from module-load time to per-call in dataTransformers.ts; fixes stale fallback enrollment date if the app stays open past midnight.

  • Commit 10 (675ba935) — removes redundant manual refetch() in closeAndRefresh after unenroll; invalidateQueries on mutation success already handles the refetch, so the manual call caused a double API hit.

  • Commit 9 (2234bd4d) — adds staleTime: 5min to useInitializeLearnerHome; useInitializeLearnerHome is consumed by 15+ components and without staleTime, every mount and window focus triggers a redundant background refetch.

  • MasqueradeBar/index.jsx — frontend-base has a cleaner implementation with logic extracted into useMasqueradeBarData(). Worth investigating whether this can be backported to master.

  • data/services/lms/api.test.tsx — frontend-base mocks constants at file level with jest.mock; master does it in beforeEach. The file-level approach is cleaner and worth backporting to master.

  • data/services/lms/api.test.tsx — frontend-base has no eslint-disable for unused vars; master does. Worth investigating why and backporting the fix.

Worth porting to frontend-base

  • UnenrollConfirmModal/hooks/index.js — master migrates refreshList to useInitializeLearnerHome().refetch; frontend-base still uses queryClient.invalidateQueries. Worth switching to useInitializeLearnerHome() for consistency.

Consistency / pattern

  • StrictDict.js console logging — master removes a console.log but keeps console.error; frontend-base already has neither. Worth deciding: remove console.error from master too, or add it back to frontend-base.
  • MasqueradeBar hooks pattern — master inlines all hook logic directly into index.jsx and deletes hooks.js; frontend-base keeps a separate hooks.js with the same logic. Worth deciding on a preferred pattern and making the two consistent.
  • MasqueradeBar/hooks.js test coverage — frontend-base kept hooks.js (rewritten) but has no corresponding test file. May already be covered by other test files, but worth verifying.

Frontend-base / upstream

  • EnvironmentTypes circular dependency in site.config.test.tsx — current workaround ('test' as SiteConfig['environment']) is acceptable; worth investigating whether @openedx/frontend-base could expose a separate subpath entry point (e.g. @openedx/frontend-base/constants) for enums/constants to avoid the circular dep entirely.

Worth investigating / is this intentional?

  • Commit 19 (62b98f95) — smart retry logic: Two questions before approving: (1) Why was retry: false set in the first place in commit 8? Understanding the original reasoning may clarify whether any retrying is appropriate here. (2) Does skipping retries on 4xx provide enough benefit over React Query's default (retry: 3 for all errors) to justify the custom logic? The alternative is to remove retry entirely and let React Query's default handle it.

  • Commit 17 (3f47d3d7) — BackedDataProvider removal: BackedDataProvider came to be because Redux always kept the normal user's data in a single global store — masquerade fallback just read from the same store slice. When migrating to React Query, the natural instinct was to manually preserve that data in a context provider. This commit recognizes that the React Query cache already serves that role and queryClient.getQueryData() is the equivalent of reading from it. Two gotchas to be aware of: (1) if the cache entry gets garbage collected before masquerade failure, getQueryData() returns undefined silently — BackedDataProvider was more explicit since data lived as long as the provider; staleTime: 5min (commit 9) reduces but doesn't eliminate this risk. (2) the query key in getQueryData() must match exactly or you get undefined silently. Neither is a blocker but worth keeping in mind.

  • jest.config.js — asset mock narrowed from many extensions to .png only. Non-blocking, but worth a comment — see brian-smith-tcril's note on frontend-base PR #182 about asset file types now needing to be maintained in two places (moduleNameMapper and build target).

  • CourseCard/hooks.test.jsforwards formatMessage from useIntl and passes course title and banner URL from course data tests removed on both master and frontend-base. Presumably covered elsewhere — worth confirming.

  • Dashboard/index.test.jsx — "courses still loading, should render LoadingView" test logic changed from isPending: true to hasCourses: false on master in 6ae8180f, then was changed back on frontend-base in 21cb5186 — this commit now re-applies the isPendinghasCourses change. Intentional?

  • SocialShareMenu.test.jsx — removes expects from "is disabled" test that were added in 21cb5186 on frontend-base and never existed on master. Worth keeping?

  • CourseCardDetails/hooks.js — reverts ||?? (nullish coalescing) change introduced in 89559a49 on frontend-base, which was never on master. Is this intentional?

  • Dashboard/index.jsx — removes <div id="learnerdashboardroot"><main> wrapper (added in 89559a49 on frontend-base); it's not clear why it was added or why it is being removed.

  • react-test-renderer removed from package.json on frontend-base but kept on master. Is this intentional?

  • DashboardLayout.test.jsx — master asserts expect(sidebarCol.children[0]).toHaveAttribute('id', 'org.openedx.frontend.learner_dashboard.widget_sidebar.v1'); before this commit, frontend-base had expect(sidebarCol.children[0]).toHaveAttribute('id', 'looking-for-challenge-widget'). The PR replaces this with expect(sidebarCol.children.length).toBeGreaterThan(0) (with a comment "Slot renders its default children directly"). Checking length feels weaker than the previous assertion — putting something in the slot and asserting on it would better reflect how this was tested before.

Nits

  • UnenrollConfirmModal/hooks/index.test.js (commit 24) — several nested beforeEach blocks include mock setup (useCourseData, useUnenrollFromCourse, reasons.useUnenrollReasons) already covered by higher-level mocks; each nested block really only needs useAppConfig.mockReturnValue({ SHOW_UNENROLL_SURVEY: ... }).
  • SocialShareMenu.jsx — adds a bare React import after removing useContext, but React isn't needed; worth removing.
  • CourseBanner.test.jsx — test name initializes data with course number from enrollment, course and course run data feels misaligned with the new React Query logic; worth updating to reflect what the test actually does now.
  • src/tracking/trackers/socialShare.js — frontend-base still uses a relative import path (../../data/services/lms/api) instead of @src/data/services/lms/api.
  • ConfirmEmailBanner/hooks.js — switches from named useState import to importing React itself; not clear why the move away from destructuring.
  • SelectSessionModal/hooks.test.jsx — master has a frontend-platform logging mock with no equivalent on frontend-base. mutationHooks.test.tsx does mock logging on frontend-base, so the pattern exists — worth figuring out why it's missing in SelectSessionModal/hooks.test.jsx and establishing a consistent approach. (Check how this was resolved in the authn PR review.)
  • data/context/index.tsx and index.test.tsx — frontend-base removes MasqueradeProvider from the provider tree and re-exports it instead; the test drops the corresponding Masquerade Available assertion. The masquerade assertion removal is likely fine since it's handled via a hook on frontend-base and tested elsewhere, but the overall changes to the context index aren't clear — worth understanding why MasqueradeProvider is being exported rather than composed, and whether the test coverage is still adequate.
  • Masquerade.test.tsx and SelectSession.test.tsx — unicode test strings encoded differently between master and frontend-base. Masquerade.test.tsx: 'José.García@université.fr' (master) vs 'Jos\u00e9.Garc\u00eda@universit\u00e9.fr' (frontend-base). SelectSession.test.tsx: 'card-节点-123' (master) vs 'card-\u8282\u70b9-123' (frontend-base). Functionally identical but worth deciding on a consistent convention for unicode in test data.
  • UnenrollConfirmModal/hooks/index.test.jscalls initializeApp api method test now asserts on mockRefreshList but the test name still says "initializeApp"; test name should be updated to match.
  • PendingContent.test.jsx — removes expect(button).toHaveAttribute('aria-disabled', 'true') with no master equivalent; line was added in 21cb5186 on frontend-base. Worth investigating why it was added and whether it should be kept.
  • UnenrollConfirmModal/hooks/index.test.js — removes calls refreshList and close test from closeAndRefresh block; was added in 21cb5186 on frontend-base with no master equivalent. Worth keeping?
  • UnenrollConfirmModal/hooks/index.test.jsmodalState tests rewritten; version from 21cb5186 on frontend-base may be preferable. Worth comparing.

Master only

Restore accidental reverts from the master port: Dashboard wrapper
div and main landmark, test assertions for disabled state and
aria-disabled, and initIsPending in Dashboard test.

Clean up nits: remove unused React imports, replace escape sequences
with literal unicode, simplify ConfirmEmailBanner hooks, fix test
names, use @src alias in socialShare, and deduplicate beforeEach
mocks in UnenrollConfirmModal tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@arbrandes
Copy link
Copy Markdown
Contributor Author

arbrandes commented Mar 18, 2026

Once again, thanks for the review!

Definitely discuss

Smart retry logic (commit 19)

retry: false was just a conservative choice. The smart retry adds resilience for server/network errors while failing fast on 4xx (common during masquerading). It's a 3-liner using React Query's official API, with tests - we should keep it.

BackedDataProvider removal (commit 17)

Good points, but I think we're good:

On GC: gcTime (default 5min) is what controls cache eviction, and the normal user's data was actively queried moments before masquerading starts, so it won't be collected before it's needed.

On key mismatch: the fallback uses the same learnerDashboardQueryKeys.initialize(undefined) factory that produced the original cache entry. No string literals involved.

Worth investigating

CourseCardDetails/hooks.js - || vs ??

Actually intentional. The ?? change in the original frontend-base migration was wrong — empty strings should trigger the fallback (e.g. an empty provider name should show "Unknown"). Keeping ||.

Dashboard/index.jsx - wrapper div removal

Unintentional removal - added <div id="learnerdashboardroot"><main> back. (I'm not 100% sure why it was added in the first place, but I'd rather tackle this later on, if necessary.)

Dashboard/index.test.jsx - hasCourses: false vs isPending: true

Restored initIsPending: true. The test renders LoadingView because of the pending state, not because of empty courses - the frontend-base version was clearer.

SocialShareMenu.test.jsx - removed expects

Restored the toHaveClass('disabled') assertion.

CourseCard/hooks.test.js - removed tests

These tested Redux hooks (reduxHooks.useCardCourseData) that no longer exist after the React Query migration. Correctly removed.

DashboardLayout.test.jsx - weaker sidebar assertion

Intentional. The sidebar now uses a WidgetSidebarSlot (plugin slot) whose children don't have a predictable id attribute, so we can't assert on specific children.

react-test-renderer

Intentionally removed. It's deprecated and incompatible with React 19. All tests use @testing-library/react.

jest.config.js - asset mock narrowed

Only .svg and .png are mocked because those are the only asset types currently imported in tests. If others are needed, tests will fail loudly (as they should).

Also, see my reply to that comment in the other PR.

Nits

UnenrollConfirmModal/hooks/index.test.js - nested beforeEach

Fixed. Moved the common mock setup into the outer beforeEach so each nested block only sets useAppConfig with its specific value.

SocialShareMenu.jsx - bare React import

Removed.

CourseBanner.test.jsx - test name

Renamed to "calls useCourseData with the correct cardId" to match what it actually tests.

socialShare.js - relative import

Switched to @src/data/services/lms/api.

ConfirmEmailBanner/hooks.js - React import

Simplified the whole file. I never liked the StrictDict/module.state/React.useState indirection, anyway. Replaced with direct useState calls.

SelectSessionModal/hooks.test.jsx - logging mock

Already present (logError: jest.fn()), just not asserted on. No change needed.

Masquerade.test.tsx and SelectSession.test.tsx - unicode encoding

Replaced escape sequences with literal characters for readability.

UnenrollConfirmModal/hooks/index.test.js - test name

Renamed the closeAndRefresh test to "behaves the same as close", since the implementation aliases closeAndRefresh to close.

PendingContent.test.jsx - aria-disabled assertion

Restored the expect(button).toHaveAttribute('aria-disabled', 'true') assertion.

UnenrollConfirmModal/hooks/index.test.js - removed calls refreshList and close test

Not restoring. The old test validated a separate refreshList call, but closeAndRefresh is now aliased to close - there's no separate refresh step.

data/context/index.tsx - MasqueradeProvider re-exported

Intentional. MasqueradeProvider is composed at the app level via src/providers.ts (frontend-base's AppProvider array), not in ContextProviders. The re-export is for import convenience.

Unnecessary React imports

Replaced import React from 'react' with import type { ReactNode } from 'react' in test files and data/context/index.tsx where React was only used for the ReactNode type.

Copy link
Copy Markdown

@holaontiveros holaontiveros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't run a full per commit review as Brian, but I did manually test all the paths I could and didn't found any error or something concerning right away,

This was before the last round changes after Brian review but it doesn't seem those changes should have change any behaviour

@brian-smith-tcril
Copy link
Copy Markdown
Contributor

Smart retry logic (commit 19)

retry: false was just a conservative choice. The smart retry adds resilience for server/network errors while failing fast on 4xx (common during masquerading). It's a 3-liner using React Query's official API, with tests - we should keep it.

After reading TanStack/query#372 it makes sense that React Query doesn't include this logic, so implementing it on our side seems reasonable. Reading that thread did make me wonder if we should be passing in everything as part of the object for useQuery or if we should try to utilize queryClient's defaultOptions. I think doing it the way we are now for this app is fine, as we only have the one query. I was initially concerned that setting retry in defaultOptions might also override useMutation's default value for retry (which is 0), but it seems that pulls from .mutations while queries pull from .queries (src). It's also possible that using defaultOptions could complicate testing, but it's probably worth looking into as we start migrating repos with more queries.


BackedDataProvider removal (commit 17)

Good points, but I think we're good:

On GC: gcTime (default 5min) is what controls cache eviction, and the normal user's data was actively queried moments before masquerading starts, so it won't be collected before it's needed.

On key mismatch: the fallback uses the same learnerDashboardQueryKeys.initialize(undefined) factory that produced the original cache entry. No string literals involved.

On the GC point: 👍. Seems perfectly reasonable.

For the key mismatch potential, I also think we're fine, I just want to walk through the flow to make sure I'm understanding why it won't be an issue (and that we aren't setting ourselves up to accidentally introduce an issue going forward).

On first load, the initial state of masqueradeUser from MasqueradeProvider is undefined because of

const [masqueradeUser, setMasqueradeUser] = useState<string | undefined>(undefined);

So when we first get into useInitializeLearnerHome

const query = useQuery({
queryKey: learnerDashboardQueryKeys.initialize(masqueradeUser),

effectively becomes

  const query = useQuery({
    queryKey: learnerDashboardQueryKeys.initialize(undefined),

which matches the fallback we have later

if (masqueradeUser && query.isError) {
data = queryClient.getQueryData(learnerDashboardQueryKeys.initialize(undefined));
}

LGTM!


CourseCardDetails/hooks.js - || vs ??

Actually intentional. The ?? change in the original frontend-base migration was wrong — empty strings should trigger the fallback (e.g. an empty provider name should show "Unknown"). Keeping ||.

👍


Dashboard/index.jsx - wrapper div removal

Unintentional removal - added <div id="learnerdashboardroot"><main> back. (I'm not 100% sure why it was added in the first place, but I'd rather tackle this later on, if necessary.)

Yeah, let's just make a small follow up issue to look into this. Adding it back in e175088 seems good for now.

👍


Dashboard/index.test.jsx - hasCourses: false vs isPending: true

Restored initIsPending: true. The test renders LoadingView because of the pending state, not because of empty courses - the frontend-base version was clearer.

👍, might be worth a backport


SocialShareMenu.test.jsx - removed expects

Restored the toHaveClass('disabled') assertion.

👍


CourseCard/hooks.test.js - removed tests

These tested Redux hooks (reduxHooks.useCardCourseData) that no longer exist after the React Query migration. Correctly removed.

Happy with the removal, mostly wondering about the "Presumably covered elsewhere" part. Assuming course title and banner URL have adequate coverage this LGTM!

👍


DashboardLayout.test.jsx - weaker sidebar assertion

Intentional. The sidebar now uses a WidgetSidebarSlot (plugin slot) whose children don't have a predictable id attribute, so we can't assert on specific children.

I don't love that it's less explicit, but that feels like a rabbit hole. Might be worth making an issue on frontend-base to think about how to support more explicit testing for things like this in the future.

For now this LGTM!

👍


react-test-renderer

Intentionally removed. It's deprecated and incompatible with React 19. All tests use @testing-library/react.

I guess my actual question here was, "is not removing it on master intentional?"

If we can remove it on master that seems like it's worth a backport.

👍


jest.config.js - asset mock narrowed

Only .svg and .png are mocked because those are the only asset types currently imported in tests. If others are needed, tests will fail loudly (as they should).

Also, see my reply to that comment in the other PR.

Sounds good. I replied to your reply in that PR and approved it openedx/frontend-base#182 (review)

👍


UnenrollConfirmModal/hooks/index.test.js - nested beforeEach

Fixed. Moved the common mock setup into the outer beforeEach so each nested block only sets useAppConfig with its specific value.

👍


SocialShareMenu.jsx - bare React import

Removed.

👍


CourseBanner.test.jsx - test name

Renamed to "calls useCourseData with the correct cardId" to match what it actually tests.

👍


socialShare.js - relative import

Switched to @src/data/services/lms/api.

👍


ConfirmEmailBanner/hooks.js - React import

Simplified the whole file. I never liked the StrictDict/module.state/React.useState indirection, anyway. Replaced with direct useState calls.

Much cleaner. Love it!

👍


SelectSessionModal/hooks.test.jsx - logging mock

Already present (logError: jest.fn()), just not asserted on. No change needed.

Makes sense. My full comment about that was:

SelectSessionModal/hooks.test.jsxmaster has a frontend-platform logging mock with no equivalent on frontend-base. mutationHooks.test.tsx does mock logging on frontend-base, so the pattern exists — worth figuring out why it's missing in SelectSessionModal/hooks.test.jsx and establishing a consistent approach. (Check how this was resolved in the authn PR review.)

but looking at this now I'm not even sure what I was seeing/referring to.

LGTM!

👍


Masquerade.test.tsx and SelectSession.test.tsx - unicode encoding

Replaced escape sequences with literal characters for readability.

👍


UnenrollConfirmModal/hooks/index.test.js - test name

Renamed the closeAndRefresh test to "behaves the same as close", since the implementation aliases closeAndRefresh to close.

👍


PendingContent.test.jsx - aria-disabled assertion

Restored the expect(button).toHaveAttribute('aria-disabled', 'true') assertion.

👍


UnenrollConfirmModal/hooks/index.test.js - removed calls refreshList and close test

Not restoring. The old test validated a separate refreshList call, but closeAndRefresh is now aliased to close - there's no separate refresh step.

👍


data/context/index.tsx - MasqueradeProvider re-exported

Intentional. MasqueradeProvider is composed at the app level via src/providers.ts (frontend-base's AppProvider array), not in ContextProviders. The re-export is for import convenience.

👍


Unnecessary React imports

Replaced import React from 'react' with import type { ReactNode } from 'react' in test files and data/context/index.tsx where React was only used for the ReactNode type.

👍


Copy link
Copy Markdown
Contributor

@brian-smith-tcril brian-smith-tcril left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@arbrandes arbrandes merged commit 757064c into openedx:frontend-base Mar 23, 2026
6 checks passed
@arbrandes arbrandes deleted the frontend-base-master-ports branch March 23, 2026 19:56
@arbrandes
Copy link
Copy Markdown
Contributor Author

Merged, and created the following follow-up issues:

@openedx-semantic-release-bot
Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0-alpha.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

9 participants