test: make the courseware redirect-rule tests actually assert - #2079
Merged
Merged
Conversation
Thirteen redirect-rule tests in CoursewareContainer.test.jsx called waitFor without awaiting it, so their assertions ran after the test had already passed and any failure was dropped. Most also asserted on local jest.fn() mocks (getSequenceForUnitDeprecated, getResumeBlock) that were never wired to the code under test and so could never have been called. Await the waitFor calls, assert on the request the rule actually makes (axiosMock.history) instead of the unwired mocks, and correct the four expectations that never matched the rule: the unit-to-sequence redirect keeps the unit id and the /preview prefix; the resume "returns" case needs no fallback sequence id; the isPreview-false resume case has no /preview prefix; and the isPreview-false unit-to-sequence block mocked a response shape the api has not read since #803. Test-only. Pre-existing since #1501; surfaced in review of #2074, which moves these tests to redirects.test.ts. Closes #2078 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
added this pull request to stack #2080
September 19, 2026 19:02
10 tasks
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2079 +/- ##
=======================================
Coverage 93.78% 93.78%
=======================================
Files 367 367
Lines 6048 6048
Branches 1433 1433
=======================================
Hits 5672 5672
Misses 359 359
Partials 17 17 ☔ 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 19, 2026 19:08
This was referenced Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make the 13 async redirect-rule tests in
CoursewareContainer.test.jsxactually assert. TheirwaitForcalls were never awaited, so every assertion inside them was vacuous, and most asserted on localjest.fn()mocks that nothing ever called. Test-only;CoursewareContainer.tsxis untouched. Stacked below #2074, which moves these tests intoredirects.test.ts— with this layer underneath, the moved tests arrive already correct. Part of #1946 as test hygiene on code the migration touches. Closes #2078.What changed
waitForcalls and made their testsasync:checkUnitToSequenceUnitRedirect(4 + 3 tests across theisPreviewtrue/false blocks) andcheckResumeRedirect(4 + 2).const getSequenceForUnitDeprecated = jest.fn()and twoconst getResumeBlock = jest.fn()declarations were never passed to the code under test, soexpect(mock).toHaveBeenCalled()could never pass. They now assert on the request the rule actually makes (axiosMock.history.getcontains the endpoint) or, for the not-a-unit case, that no request was made. The two resume "returns" cases wait for the request, flush a tick, then assertnavigatewas not called.isPreviewtrue › unit-to-sequence: expected/course/courseId/sequence_1; the rule navigates to the parent sequence and the unit, with the/previewprefix the same PR introduced →/preview/course/courseId/sequence_1/unit_1.isPreviewtrue › resume "returns after calling getResumeBlock": passedfirstSequenceId: 'sequence_1', so the rule's fallback navigated there; now passesnullso the case tests the no-navigation path it names.isPreviewfalse › resume "calls navigate with unitId": expected a/previewURL withisPreviewfalse →/course/courseId/section_1/unit_1.isPreviewfalse › unit-to-sequence: mocked aparent: {...}response shape the api has not read since feat: stop calling course blocks rest API and assume LS exists #803 (2022), so the rule hit its error branch; now mocks theblocksshape, passesisPreview: falseas the block name says, and expects/course/courseId/sequence_1/unit_1. ItsapiUrlis now thehrefstring like the sibling block, so the request assertion can match.Testing
npm run types(0 errors),npm run lint(clean);CoursewareContainer.test.jsx70/70 on this layer. With #2074 rebased on top,redirects.test.tscarries the same fixes and passes 51/51 with everywaitForawaited.Provenance
Introduced with the tests themselves in #1501 (2024-10-28); untouched since, so the redirect rules the migration extracted in #2074 had never actually been exercised by these tests. Flagged by @arbrandes in review of #2074 (comment); split out as its own layer below #2074 so that PR stays a faithful move and this change is reviewable on its own.
Decisions
Full decision log
Decisions — make the courseware redirect-rule tests actually assert (#2078)
A layer below refactor: tear down the courseware Redux slice #2074, not inside it and not a PR against master. The
13 un-awaited
waitForcalls predate the migration (feat: add functionality to see unit draft preview #1501, 2024-10-28) andrefactor: tear down the courseware Redux slice #2074 only moves them, so fixing them inside refactor: tear down the courseware Redux slice #2074 would blur a faithful
move with a test rewrite. A plain PR against master would keep refactor: tear down the courseware Redux slice #2074 clean
too, but refactor: tear down the courseware Redux slice #2074 would only pick the fix up after that PR landed; as a stack
layer underneath, refactor: tear down the courseware Redux slice #2074 rebases onto it now and lands with the moved tests
already correct. So:
gh stack unstack,gh stack init --base master bsmith/redirect-tests-assert bsmith/courseware-slice-teardown progress-exam-attempts-query, fix on the new bottom,gh stack rebase --no-trunk. The fix is therefore written against master's file(
CoursewareContainer.test.jsx, positionalcheck*signatures); refactor: tear down the courseware Redux slice #2074'smove carries it into
redirects.test.ts(options-object signatures) viathe conflict resolution: take refactor: tear down the courseware Redux slice #2074's side of the container file (the
tests leave it) and re-apply the same corrections to
redirects.test.tsfrom a patch prepared earlier in refactor: tear down the courseware Redux slice #2074's shape.
awaitalone would have turned vacuous tests into failing ones: theunwired mocks become request assertions on
axiosMock.history. Sixtests declared
const getSequenceForUnitDeprecated = jest.fn();and twoconst getResumeBlock = jest.fn();— fresh local mock functions thatmerely share a name with the api functions. Nothing connected them:
not passed to the rule, no
jest.mockof the module, nojest.spyOnonthe export. The rules import the real functions from
./data/apiatmodule load and call those regardless of what a local variable in the
test is named, so
expect(mock).toHaveBeenCalled()asked whether anunused local was called — always no, hidden only because the un-awaited
waitFordropped the failure.What the author wanted to check — "the rule looked up the parent sequence
/ fetched the resume block" — is observable one layer down without any
mock on the function: the real api function's only side effect before
parsing is
getAuthenticatedHttpClient().get(url), and the tests alreadyintercept that transport with
axiosMock.onGet(...). axios-mock-adapterrecords each handled request in
axiosMock.history.get(a fresh adapterper test in
beforeEach, so the history is per test), so the assertionbecomes "a GET to the endpoint is in the history":
expect(axiosMock.history.get.map((req) => req.url)).toContain(apiUrl).The not-a-unit branch must short-circuit without touching the network, so
its
not.toHaveBeenCalled()becameexpect(axiosMock.history.get).toHaveLength(0)— stronger than the original would have been even if wired, since it rules
out any request.
Why not wire the mock properly instead:
jest.mock('./data/api')wouldreplace the module for the whole file, and the container-rendering tests in
the same file drive the real api functions against the mocked network;
jest.spyOnon the namespace import works under Babel's CJS interop but isfragile and would still need a
mockImplementationto keep the real lookup(or no parent is ever found). The history check needs no module surgery and
exercises the real function end to end — including its
Object.values(data.blocks)parsing, which is what exposed theparent: {...}response mocks in the other block as never having matchedthe api (entry 3). It also required the second block's
apiUrlto be thehrefstring like the first block's, not aURLobject, since historyentries store the URL as a string.
The two
checkResumeRedirect"returns" cases wait for the request, flushone macrotask (
setTimeout(resolve, 0)) so the.thenafter the mockedresponse has run, then assert
navigatewas not called — asserting "notcalled" inside
waitForwould pass trivially on the first poll.The rule is the source of truth for the four expectations that had never
matched it.
git log -SshowscheckUnitToSequenceUnitRedirect's andcheckResumeRedirect'snavigatetargets unchanged since feat: add functionality to see unit draft preview #1501 (onlyrefactor: tear down the courseware Redux slice #2074's move touches those lines), and the pre-feat: add functionality to see unit draft preview #1501 version already
navigated unit-to-sequence to
/course/:courseId/:parentId/:unitId. Soevery correction is test-side. Direction was decided by reachability and by
the describe block each test sits in:
isPreviewtrue › unit-to-sequence). Expected/course/courseId/sequence_1. The rule has exactly threenavigatetargets —
${sequenceUrl}/${unitId}, or/course/:courseIdin theno-parent and not-a-unit branches — and none of them is a parent-sequence
URL without the unit, so no argument values could reach the original
expectation; the URL side had to be wrong. The test's own name, "parentId
and sequenceId", is the pair the rule joins (
sequenceIdhere isunit_1). Corrected to include/unit_1./previewprefix (same test). The prefix depends only on the ninthargument,
isPreview, which the test passes astrue, and the test sitsin
describe('isPreview equals true')— the block feat: add functionality to see unit draft preview #1501 created for theprefixed branch it introduced. Params and block agree the scenario is
preview-on, so the URL must carry
/preview. Flipping the argumentinstead would make a test in the "true" block exercise the "false"
branch, which the sibling block covers. Corrected to
/preview/course/courseId/sequence_1/unit_1.isPreviewfalse ›unit-to-sequence). Its three tests passed
trueforisPreviewinsidedescribe('isPreview equals false'). The block is the author's statedintent; the argument is a copy-paste artifact. Flipped to
false; thefirst test now expects
/course/courseId/sequence_1/unit_1. Between thetwo blocks both prefix branches are exercised, which the original never
managed.
isPreviewfalse › unit-to-sequence). Those mocksanswered
parent: { id }/parent: { children }.getSequenceForUnitDeprecatedhas read
Object.values(data.blocks)since feat: stop calling course blocks rest API and assume LS exists #803 (2022-02-17), two yearsbefore the tests were written, so the "found parent" case threw inside
the api and hit the error branch — the only reason its sibling "no parent
id" case coincidentally passed. Both now mock the
blocksshape the apireads; the "no parent" case exercises the real no-parent branch (the
apiUrltype fix is in entry 2).isPreviewtrue).Passed
firstSequenceId: 'sequence_1'with a response carrying neithersectionIdnorunitId, so the rule'selse if (firstSequenceId)fallback navigated to
/course/courseId/sequence_1— the test's premise("returns") contradicted its inputs. Now passes
null, the one inputunder which the rule does nothing, which is what the sibling "calls
navigate with firstSequenceId" case already covers from the other side.
isPreviewfalse). Expected/preview/course/courseId/section_1/unit_1withisPreview: false;the rule prefixes only when true. Corrected to
/course/courseId/section_1/unit_1.Test-only.
CoursewareContainer.tsxandredirects.tsare untouched;the diff is one file (56 insertions, 55 deletions) on this layer, and
redirects.test.tsdiffers from its upstream version by the equivalent 55/54once refactor: tear down the courseware Redux slice #2074 is rebased on top. Lint and types clean;
CoursewareContainer.test.jsx70/70 here,
redirects.test.ts51/51 at the top with all 13waitForcalls awaited and no
jest.fn()in the file other thannavigate.Branch name carries the
bsmith/namespace. Stack layers push to theopenedx
upstreamremote, so the branch isbsmith/redirect-tests-assert(first
initused the bare name; re-done).progress-exam-attempts-queryand the parked
retire-course-home-slicepredate that rule.gh stack unstackleft the merged history grouped. It reported thatsome PRs were "queued for merge or have auto-merge enabled" and left local
tracking alone, but on GitHub the two open PRs did come out of the stack
while the 13 merged ones stayed grouped — which is what we wanted. Neither
open PR was queued or auto-merging; the message appears to be the tool
misreading the merged PRs. Local tracking was then dropped with
--localand re-created withinit.🤖 Generated with Claude Code