test: await every waitFor and act in Course.test and Sequence.test - #2120
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2121
September 24, 2026 20:51
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2120 +/- ##
==========================================
+ Coverage 94.59% 94.79% +0.20%
==========================================
Files 368 368
Lines 5952 5952
Branches 1418 1461 +43
==========================================
+ Hits 5630 5642 +12
+ Misses 309 297 -12
Partials 13 13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
marked this pull request as ready for review
September 25, 2026 02:55
This was referenced Sep 25, 2026
arbrandes
approved these changes
Sep 25, 2026
arbrandes
left a comment
Contributor
There was a problem hiding this comment.
Approved, with one suggestion.
Comment on lines
64
to
70
| const renderCourse = (testData, testStore) => render( | ||
| <> | ||
| <MountCourseQueryHooks courseId={testData.courseId} /> | ||
| <Course {...testData} /> | ||
| <LoadedCourse {...testData} /> | ||
| </>, | ||
| { store: testStore, wrapWithRouter: true }, | ||
| ); |
Contributor
There was a problem hiding this comment.
Give renderCourse the same MemoryRouter/Routes shape SidebarWrapper now uses. wrapWithRouter: true is a BrowserRouter at /, so useSequenceNavigationMetadata finds no courseId param and every Previous/Next renders hrefless and never disabled.
Contributor
Author
There was a problem hiding this comment.
Fourteen `waitFor` and `act(async)` calls in these two suites were not awaited, so the assertions inside them could not fail the test. Thirteen date from the frontend-build and frontend-platform upgrade in #1391; the last is the upgrade-panel assertion #1885 rewrote, keeping the shape. Awaiting them turns eleven of the fifteen Sequence tests red, and this commit makes those tests assert what they say. Two harness gaps only the dead assertions had tolerated are closed. The Sequence wrapper mounts the sequence at its course route in a MemoryRouter, as the suite's hidden-after-due test already did: the navigation reads `courseId` from the route params, and without it every Previous and Next control rendered unlinked and never disabled. Course.test's renderCourse mounts at the same route for the same reason (review suggestion); its BrowserRouter at `/` had left the navigation without a `courseId` too. Course mounts behind the same loaded-metadata gate LoadedTabPage uses, since it reads `celebrations` from the course-home metadata query on its first render; with the query still pending the celebration modals could never open. The assertions target what the default tree renders. SequenceNavigationSlot has rendered nothing by default since #1713, so the controls the tests click are the unit navigation: icon buttons above the content, links below it once the unit has loaded (disabled buttons on a first or last unit), both wired to the `'bottom'` placement handler. The unit-tab test is removed; the tabs are not rendered by default and have their own suites. The suite's mock of the special-exams thunks module is removed. Since #1391 the jest module mapper's unanchored regex has rewritten that subpath to the package directory, so the mock replaced the package entry with a spread copy that dropped the `__esModule` marker and broke the default import the moment a test rendered a loaded sequence; its stub was read by nothing. The real thunk returns immediately with no exam attempt in the store. The upgrade-panel cases render the real SidebarContextProvider with the panel stored open, since their faked context had no widget registry for Sidebar to render from. Course.test's three `it.skip` tests from #1669 run again, and its two learner-tools tests, one waiting on a null captured before the element existed and one setting the window width to an undefined Paragon breakpoint, assert now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/await-test-assertions
branch
from
September 28, 2026 05:46
7b458dc to
c7a4b6c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fourteen
waitFor/act(async …)calls inCourse.test.jsxandSequence.test.jsxwere not awaited, so their assertions could not fail; thirteen date from #1391 (August 2024) and the last from #1885. Awaiting them is a one-word change per call, and it turns eleven of the fifteenSequencetests red, so this PR is mostly the work of making those tests assert what they claim. Two harness gaps that only the dead assertions had tolerated are closed:Sequencenow renders at its course route, so the navigation gets itscourseIdparam and renders real links and disabled states, andCoursemounts behind the same loaded-metadata gateLoadedTabPageuses, so the celebration modals can open. TheSequencesuite's 2023 mock of the special-exams thunks module, which a 2024moduleNameMapperentry has been silently redirecting to the package root, is removed; the threeit.skiptests inCourse.test.jsxare un-skipped; two learner-tools tests that were vacuous for their own reasons now assert. Test-only: no runtime change. Sits below #2113 on the stack so the sidebar layers build on honest tests. Closes #2118. Closes #1669.What changed
waitFor,actandfindBy*is awaited. Six blocks that passed afindBy*promise totoBeInTheDocument()now await the control they use (decision 4).Sequence.test's wrapper mounts the sequence at/course/:courseId/:sequenceId/*in aMemoryRouter, as the suite's hidden-after-due test already did. Without the param,useSequenceNavigationMetadatabails out and every Previous/Next renders unlinked and never disabled (decision 1).SequenceNavigationSlot: icon buttons above the content, links (or disabled buttons on a first or last unit) below it afterloadUnit(), both reportingwidget_placement: 'bottom'(feat: Add SequenceBottomNavigationSlot for customizable sequence navigation #1918).handles unit navigation buttonis removed: the unit tabs it clicked are not rendered by default and have their own suites (decisions 2–3).dist/data/thunks.js, but themoduleNameMapperregex from feat: updated frontend-build & frontend-platform major versions #1391 rewrote that path to the package directory, so it replaced the package entry with a spread copy that broke the default import as soon as a test rendered a loaded sequence; its stub was read by nothing. The real thunk returns immediately with no exam attempt in the store (decision 5).SidebarContextProviderwithUPGRADEstored on a desktop viewport, and assert the panel region is present and that the close button removes it (decision 7).Course.test'srenderCoursemountsCourseonce the course-home metadata query has succeeded, the wayLoadedTabPagedoes;Coursereadscelebrationson first render. The threeit.skiptests fromCourse.test.jsxtests passing when they shouldn't #1669 run again (decisions 1, 8). Per review, it also mounts at the course route in aMemoryRouter, theSidebarWrappershape, so the navigation gets itscourseIdparam there too (decision 11).nullcaptured before the element existed; the other setinnerWidthto an undefined Paragon breakpoint, soCoursenever mounted (decision 9).Testing
npm run typesandnpm run lintclean; full suite 116 suites, 1169 passed, 0 skipped (was 3 skipped). Onmaster, adding only theawaits fails 11 of 15Sequencetests and, with the skips removed, the 3Coursetests. No manual testing: nothing outsidesrc/**/*.test.jsxchanges.Decisions
Full decision log
Decisions — await every
waitForandactinCourse.test.jsxandSequence.test.jsx(#2118)A test-only layer at the bottom of the running stack, below #2113. Fourteen
waitFor/act(async …)calls were not awaited (thirteen from #1391,August 2024; one carried by #1885), so the assertions inside them could not
fail. Awaiting them is the mechanical part; most of the tests then fail for
reasons the missing
awaithad hidden, and the entries below record what eachone was and what the test asserts now. Closes #2118 and #1669.
The two files render the way the app does. Both suites rendered the
component straight under
render(…, { wrapWithRouter: true }), whichmounts a
BrowserRouterat/.Sequence's navigation readscourseIdfrom the route params (
useSequenceNavigationMetadata→useParams()), and without it every Previous/Next control renders in itsdegenerate form: no link, never disabled, and a
previousLink is marked as requiredprop-type warning on every render.Sequence.test's wrappernow mounts the sequence inside a
MemoryRouterat/course/:courseId/:sequenceId/*, the harness the suite's hidden-after-duetest already used; the
wrapWithRouteroption goes with it.Coursereads
celebrationsfrom the course-home metadata query on its firstrender (a
useStateinitialiser and an effect keyed onsequenceId), andin the app
LoadedTabPagemounts it only once that query has succeeded;the tests mounted it beside
MountCourseQueryHookswith the query stillpending, so both celebration modals could never open.
Course.test'srenderCoursenow mountsCoursebehind the sameisSuccessgate. Neitherchange alters what the tests exercise; each removes a way the harness
differed from the app that only the un-awaited assertions had tolerated.
The navigation the tests click is the unit navigation, not the sequence
navigation. The old assertions looked for a top
Previouslink, unittabs by title and a
widget_placement: 'top'event. They described thelayout the suite's default store selected until feat: remove waffle flags for managing course outline sidebar #1713 (July 2025): with
the outline-sidebar flag off,
Sequence.jsxrenderedSequenceNavigationSlot, whose default content wasSequenceNavigation— the bar with Previous and Next as links (links since fix: make nav buttons use links for accessibility #1137, August
2023) and the unit tabs — plus
UnitNavigationbelow the content, alsolinks; two Previous links, two Next links, and
'top'from the bar. Withthe flag on, the bar was not rendered and
UnitTitleSlotrenderedUnitNavigationabove the content instead (feat: [FC-0056] courseware sidebar enhancement #1386, May 2024; icon buttonssince feat: Update previous and next unit navigation buttons design #1617, March 2025). The two layouts were exclusive. feat: remove waffle flags for managing course outline sidebar #1713 removed
the flag and kept the second layout, making
SequenceNavigationSlotanempty
PluginSlot, soSequenceNavigationis not in the default tree.What renders is
UnitNavigationtwice: above the content as icon buttons(
isAtTop), and below it, once the unit has loaded, as links, or asdisabled buttons on a first or last unit — still two Previous and two
Next controls, which is why the handler-call counts the tests assert did
not change. Both renders take the same
unitNavigationPropsobject,whose handlers are
previousHandler('bottom')/nextHandler('bottom')(feat: Add SequenceBottomNavigationSlot for customizable sequence navigation #1918, June 2026);
isAtTopchanges the markup, not the handler. The'top'handlers stillexist, as
sequenceNavProps, but go only to the empty slot, so nothing inthe default tree ever emits
widget_placement: 'top'. The tests assertwhat fires: the button above, the link below after
loadUnit(), and'bottom'for both. Whether the upper unit navigation should report'top'is a product question this layer records and does not answer; itis runtime behaviour, and the fix would be to give the
isAtToprenderits own handlers.
handles unit navigation buttonis removed. It clicked a unit tab byits title. The tabs live in
SequenceNavigation, which the default slotdoes not render (entry 2), and
Sequence.jsxmarks itsonNavigatehandler
istanbul ignorefor that reason. The tab components have theirown suites (
SequenceNavigationTabs.test,UnitButton.test,SequenceNavigationDropdown.test).expect(screen.findByText(…)).toBeInTheDocument()becomes an awaitedfindBy. Six blocks passed the promise afindBy*returns to thematcher, which can never pass. The statements stay in master's order;
each gets the
awaitit needs: the loading text is awaited, the controla test clicks is awaited (
await screen.findByRole('button', { name: /previous/i })), and afterloadUnit()the loading text's absence isawaited in a
waitForbefore the lower control is used. A loading stateand a loaded state inside one
waitForcallback could never both hold ona single retry. The clicks in the rewritten tests go through
userEventrather thanfireEvent, the convention the newer suites use;the two disabled-button tests click both buttons through
Promise.all.The special-exams mock is removed; the mapper stays. The suite's
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', …)hasnot mocked that file since feat: updated frontend-build & frontend-platform major versions #1391 added a
moduleNameMapperentry whoseunanchored regex rewrites the subpath to the package directory: the mock
replaced the package entry with
{ ...realEntry, checkExamEntry }, whosespread drops Babel's non-enumerable
__esModulemarker, so the defaultimport in
Sequence.jsxreceived the whole object and React failed with"Element type is invalid" the moment a test rendered the loaded sequence
(none did, until now). The stub itself was read by nothing:
ExamWrapperimports the thunk from the library's internal
datamodule. With themock gone the real
checkExamEntryruns, readsgetState().specialExams.examand returns on its first line, because the test store has no attempt.
The mapper's regex is left as it is: the package's
exportsmap has onlyan
importcondition, so Jest needs the mapping for the root import, andanchoring it is a
jest.config.jschange with no remaining user in thislayer.
Each
Sequencetest that renders a loaded sequence owns its store.initializeTestStoreregisters the axios routes for its own course on ashared adapter every time it is called, whether or not it replaces the
global store, so the routes belong to whichever test called it last.
handles loading unitrendered against the suite-level store and couldonly load its sequence if no earlier test had re-registered the routes;
it now builds its own store and takes
sequenceIdfrom it, as thenavigation and gated tests already did.
The upgrade-panel cases render the real
SidebarContextProvider.Their faked context (
{ courseId, currentSidebar: 'UPGRADE', toggleSidebar })had no
SIDEBARSregistry, andSidebar.jsxreturnsnullwithout one,so the panel could not render under it. They now mount the provider with
UPGRADEstored in localStorage on a desktop-width viewport, assert thepanel's region is present, and that the close button removes it. Make useSidebar() the only sidebar context read and test the sidebar suites under the real provider #2112's
conversion of this suite to the provider builds on this shape.
Course.test's three skipped tests are un-skipped.loads learning sequencewaits for the loading text, then for the unit's title headingbefore posting the loaded message — the heading mounts in the same render
as the iframe whose hook listens for that message, and the message is
lost if posted earlier — then for the loading text to clear, and checks
the page title. The
two celebration tests await the dialog. The
it.skipcomments citingCourse.test.jsxtests passing when they shouldn't #1669 go with the skips.Two learner-tools tests in
Course.testwere vacuous for their ownreasons. The wide-viewport test captured
queryByTestIdbefore theelement existed and then waited on that
null; it awaitsfindByTestId.The narrow-viewport test set
innerWidthtobreakpoints.extraSmall.minWidth,which Paragon does not define, so the width was
undefinedandCourse(which returns
nullfor an undefined width) never mounted; it now usesbreakpoints.extraSmall.maxWidth, passes aunitIdlike its sibling,waits for the sequence to render, and then asserts the tools are absent.
The suite's default width moves from
beforeAllto abeforeEach, so atest added after this one starts from the desktop width rather than
inheriting the narrow one.
Gated content keeps its assertions, with today's values. The old block
asserted the locked-content loading text present and, in the same
callback, absent, and counted three buttons ("Previous, Prerequisite and
Close Tray") and one link ("Next"). The loading text is the Suspense
fallback for the lazy-loaded
ContentLock; it is present for an instantand gone once the chunk resolves, so the test finds it first, then waits
for "Content Locked", then asserts it has cleared. The counts, under the
original comment lines, are now two buttons and no links, and the reason
is feat: remove waffle flags for managing course outline sidebar #1713 (July 2025): before it,
SequenceNavigationSlot's defaultcontent was
SequenceNavigation, the bar with Previous, the unit tabs(a single lock tab when gated) and Next, which is where the gated view's
Previous button and Next link came from; feat: remove waffle flags for managing course outline sidebar #1713 made the slot an empty
PluginSlot, so the bar renders only for an operator who plugs it in.In the normal view the unit navigation above and below the content
stands in for it. In the gated view there is no unit, only
ContentLock,so nothing does: a learner on a locked sequence has had no Previous or
Next since then. The test could not report it because its counts had
sat inside an un-awaited
waitForsince feat: updated frontend-build & frontend-platform major versions #1391 the year before. Whetherthe gated view should get its navigation back is a product question this
layer records and does not answer.
Course.test'srenderCoursemounts at the course route (reviewsuggestion on PR test: await every waitFor and act in Course.test and Sequence.test #2120, 2026-09-25, applied 2026-09-28). The helper
rendered with the shared
render'swrapWithRouter: true, which isAppProvider'sBrowserRouterat/, souseSequenceNavigationMetadataread no
courseIdfromuseParams()and every Previous / Next controlrendered without an
hrefand never disabled — the same gap entry 1closed for the Sequence suite's wrapper, and pre-existing here. The
helper now wraps its two children in a
MemoryRouterat/course/{courseId}/{sequenceId}/{unitId}with aRouteat/course/:courseId/:sequenceId/*, theSidebarWrappershape. Allnineteen cases pass unchanged: the one navigation case, passes
handlers to the sequence, only counts handler calls, which is why the
hrefless links never failed it.
🤖 Generated with Claude Code