test: make the useIsCourseLoaded tests actually assert - #2101
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2080
September 23, 2026 01:24
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2101 +/- ##
=======================================
Coverage 93.93% 93.93%
=======================================
Files 366 366
Lines 5916 5916
Branches 1427 1384 -43
=======================================
Hits 5557 5557
- Misses 345 346 +1
+ Partials 14 13 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Four `useIsCourseLoaded` tests, for a pending query, a learner without access, a failed outline and a failed metadata request, registered their special handler and then called `mockHappyPath()`. axios-mock-adapter replaces an existing handler with the same method and URL, so the special handler was gone before the hook rendered and all three queries succeeded. The tests passed anyway because they asserted `false` as soon as three requests had been dispatched, before anything settled, and the hook is `false` at that moment for every input. Deleting the four special handlers left them green. Each now registers its handler after the happy path, waits for the queries to settle rather than for requests to be sent, and asserts its own precondition (the metadata query is pending; the outline or metadata query is in error; `courseAccess.hasAccess` is false) before asserting the result, so a test whose setup stops applying fails instead of passing. `renderLoaded` takes the client so the tests can inspect it. Deleting any one special handler now fails its test. The hook itself is unchanged. Part of #1946 (Stage 1). Closes #2100. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/course-loaded-tests-assert
branch
from
September 23, 2026 11:32
543e1ff to
e5eb58c
Compare
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 four
useIsCourseLoadedtests incourseware/data/apiHooks.test.tsxactually assert. Each registered its special handler (a pending request, a 403, a 500,has_access: false) and then calledmockHappyPath(), which replaced it, and each assertedfalseas soon as three requests had been dispatched, before any response arrived. So they never exercised the state they are named for: deleting the four special handlers left all four green. Test-only;apiHooks.tsis untouched. Stacked on #2099 (its one-argument metadata key) and below #2098, whose owner/reader split is what exposed them. Part of #1946 as test hygiene on code the migration touches. Closes #2100.What changed
mockHappyPath(). axios-mock-adapter keeps one handler per method and URL and splices a new one over an existing match, so registering last is what makes the pending/403/500/no-access handler the one that answers.queryClient.isFetching()to reach 0; the pending case, whose client never goes idle, waits for the other two queries to succeed.pending; the outline or metadata query is inerror;courseAccess.hasAccessisfalse. A test whose setup stops applying now fails instead of passing.renderLoadedtakes the client as an optional argument, so the tests can inspect the client the hook is using; the two tests that only readresult.currentare unchanged. A smallstatusOfhelper readsgetQueryState(…)?.status.Testing
npm run typesandnpm run lintclean;apiHooks.test.tsx38/38; full suite 113 suites, 1137 passed, 3 skipped. Checked both directions on this layer's base: with the old tests, deleting the four special handlers leaves them green; with the new ones, deleting any special handler fails its test.Provenance
All four arrived in #2071 ("derive the courseware loaded gate and sequence ids from queries"). They surfaced while implementing #2098: splitting
useIsCourseLoaded's observers into an owner that fetches and a reader that does not changes when the queries settle relative to the old assertion, so the reader saw the happy-pathtrueand all four failed. Split out as its own layer, like #2078 / #2079, so #2098 stays about who fetches.Decisions
Full decision log
Decisions — make the useIsCourseLoaded tests actually assert (#2100)
A layer of its own, between Take tab identity out of the course-home metadata query #2084 and Stop the courseware gate queries refetching from components under the gate #2098. The four vacuous tests came
from refactor: derive the courseware loaded gate and sequence ids from queries #2071; Stop the courseware gate queries refetching from components under the gate #2098 only exposed them. Its owner/reader split changes when the
gate queries settle relative to the old assertion, so all four went red on
that layer without the hook doing anything wrong. Fixing them inside Stop the courseware gate queries refetching from components under the gate #2098
would mix a repair of refactor: derive the courseware loaded gate and sequence ids from queries #2071's tests into a change about who fetches, so it
is split out the way Make the courseware redirect-rule tests actually assert: un-awaited
waitForand unwired mocks from #1501 #2078 (PR test: make the courseware redirect-rule tests actually assert #2079) was for the redirect-rule tests. Itsits above Take tab identity out of the course-home metadata query #2084 because the precondition assertions build
courseHomeQueryKeys.metadata(courseId), the one-argument key Take tab identity out of the course-home metadata query #2084introduced (master's still takes
rootSlug), and below Stop the courseware gate queries refetching from components under the gate #2098 so that layerlands on tests that already check something.
Two defects, and fixing either alone is not enough.
then called
mockHappyPath(). axios-mock-adapter 2.1.0 keeps one handlerper method and URL:
addHandler(src/index.js:272-277) splices a newhandler over an existing match rather than adding a second, so the
happy-path 200 replaced the pending, 403, 500 or
has_access: falsehandler before the hook rendered.
waitFor(() => expect(axiosMock.history.get.length).toBeGreaterThanOrEqual(3))passes once three requests have been dispatched, which happens as the
hook's observers subscribe on mount. The mock responses resolve
asynchronously after that, so the queries were still
pendinganduseIsCourseLoadedwasfalsefor every input. Thefalsethe testschecked was the hook's answer before anything had loaded.
Deleting the four special handlers outright left all four green, on this
layer's base with no other change. Swapping the order alone would still
assert before anything settled; waiting alone would wait for the
happy-path responses and find
true.The special handler is registered after
mockHappyPath(). It thenreplaces the happy-path handler for its URL instead of being replaced by it.
mockHappyPath()still registers all three URLs, so the two queries a testis not about keep succeeding.
Wait for the queries to settle, not for requests to be sent. The three
failure and no-access tests wait for
queryClient.isFetching()to reach 0,so the
falsethey assert is the hook's answer to the real outcome, and ahook that treated a failed query as loaded would read
truethere and fail.The pending test cannot wait for idle, because its never-resolving request
keeps the client fetching; it waits for the other two queries to succeed
and then checks that the metadata query is still
pending.Each test asserts its own precondition before the result. The metadata
query is
pending; the outline or metadata query is inerror;courseAccess.hasAccessisfalse. This is what stops defect 1 fromcoming back unnoticed: if the handlers are reordered again, or a URL stops
matching, the precondition assertion fails instead of the test silently
running the happy path. Deleting any one special handler now fails its
test.
renderLoadedtakes the client as an optional argument. The tests haveto inspect the client the hook is using (
isFetching,getQueryState,getQueryData), andrenderLoadedbuilt one inline, inside the wrapper,with no reference out. It now accepts one, defaulting to
createTestQueryClient(), so the two tests that only readresult.current(is true once all three queries resolve, is false without a courseId)
are unchanged. As a side effect the client is created once per test rather
than inside the wrapper's render — that inline construction was only safe
because
renderHookre-renders the hook's component and not its wrapper.statusOfis a small local helper for the threegetQueryState(…)?.statusreads.
The explicit per-test waits were kept over two shorter shapes.
renderHook, so each test waits onresult.current.outline.isErrorand needs no client reference. Rejectedbecause it changes the shape of every test in the block — the two
healthy tests would move to
result.current.isLoaded— and a singleprecondition wait does not guarantee the other two queries have settled;
making it as strict as
isFetching() === 0puts the lines back.renderSettledhelper that creates the client, renders and waits foridle. Rejected as a new helper for three uses that the pending test
cannot share.
Flushing promises or timers (
await act(() => new Promise(setImmediate)))was not considered further: it is a timing assumption, the kind that made
the old tests vacuous. Hand-resolved
replypromises give more control butcost more code than either shape above.
Test-only.
courseware/data/apiHooks.tsis untouched; the diff issrc/courseware/data/apiHooks.test.tsxalone (36 insertions, 14deletions).
npm run lintandnpm run typesclean;apiHooks.test.tsx38/38; full suite 113 suites, 1137 passed, 3 skipped.🤖 Generated with Claude Code