fix(site): deflake adjust user theme preference - #28219
jeremyruppel merged 3 commits into
Conversation
The "adjust user theme preference" e2e test selected the Light theme and then hard-navigated with page.goto before the optimistic appearance update was persisted. The navigation could cancel the in-flight PUT, so the reloaded document embedded the stale dark preference and the final assertion flaked. Wait for the appearance form's save spinner to clear before the hard reload, which mirrors how other settings tests wait for a visible save confirmation. Asserting the optimistic light class first guarantees the spinner is already showing if a save started, and a repeat run that is already light never shows it, so the test is idempotent. To give the test a UI signal (instead of a direct API call), add an opt-in `label` prop to Spinner that exposes it as a role=status live region with an aria-label; spinners stay decorative when no label is given. Move Loader's label onto its own status container so it keeps a single status region, and label both appearance save spinners.
55b39e7 to
f0e009c
Compare
|
I decided to update the |
| // Precondition: the theme mode must start on "Single theme" so that picking | ||
| // a single theme below takes effect immediately. A fresh member defaults to | ||
| // single mode; assert it so the test fails loudly if that default changes. |
There was a problem hiding this comment.
so I've noticed claude comments a lot of code, and excessive comments aren't exactly helpful, especially if they are documenting code shorter than what is written or edited.
Comments can't be tested for accuracy, and they can rot faster than the code. They also add to the code that LLMs read, using more context in the future. When letting AI take a pass on writing code, consider removing or trimming down the comments, especially to just function headers, and what is necessary to document best usage.
There was a problem hiding this comment.
for sure, I left these in because I personally found them helpful, and this one in particular because it describes a failure mode the tests don't exercise. totes happy to remove it though!
Make Spinner's label prop description less LLM-y Co-authored-by: Samuel Volin <[email protected]>
untra
left a comment
There was a problem hiding this comment.
thanks for letting me pedantic about code comments 😅 👍
Summary
Deflakes the
adjust user theme preferencePlaywright test(
site/e2e/tests/users/userSettings.spec.ts), tracked in DEVEX-415.The test selected the Light theme and then hard-navigated with
page.gotobefore the optimistic appearance update was persisted. The navigation could
cancel the in-flight
PUT /api/v2/users/me/appearance, so the reloadeddocument embedded the stale
darkpreference and the final assertion flaked.toPassretries could not help because retrying the reload only re-reads thestill-stale persisted state.
Fix
Wait for the appearance form's save spinner to clear before the hard reload,
mirroring how other settings tests wait for a visible save confirmation.
Asserting the optimistic light class first guarantees the spinner is already
showing if a save started; a repeat run that is already light never shows it,
so the test is idempotent and there are no direct API calls.
To give the test a UI signal,
Spinnergets an opt-inlabelprop thatexposes it as a
role="status"live region with anaria-label(decorativeotherwise).
Loadermoves its label onto its own status container so it keepsa single status region.
Changes
site/src/components/Spinner/Spinner.tsx: opt-inlabelprop.site/src/components/Loader/Loader.tsx: label on its own status region.site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx: labelboth appearance save spinners.
site/e2e/tests/users/userSettings.spec.ts: wait for the save spinner.Validation
biome checkandtsc -p .pass.pnpm playwright:test -g "adjust user theme preference" --repeat-each 20.Implementation plan & decision log
DEVEX-415: Fix flake in "adjust user theme preference" e2e test
Problem
Playwright test
site/e2e/tests/users/userSettings.spec.ts→adjust user theme preferenceflakes. After selecting the Light theme and hard-navigatingto
/, the reloaded page sometimes staysdark, failing the finalassertion.
CI Flake Bot has recorded repeated recurrences on
main(latest 2026-08-17,runs
32004239187,31745261984) even after PR #25183 addedtoPassretries.Root cause (confirmed by reading the code)
The appearance update is optimistic and its persistence is not awaited before
navigation:
updateAppearanceSettings(site/src/api/queries/users.ts) has anonMutatethat optimistically writes the new theme into the React Querycache. The
<html>class flips tolightimmediately, before thePUT /api/v2/users/me/appearancecompletes.useQueuedAppearanceSubmit(
site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx)serializes submits: if a request is in flight, the next is queued and only
fires after the first settles.
memberuser has empty appearance settings.migrateLegacyPreference(site/src/theme/themeMode.ts) maps empty settingsto
{ mode: "single", theme: "dark" }(DEFAULT_THEME = "dark"). So the"Theme mode" dropdown already starts on Single theme and
<html>startsdark. Selecting "Single theme" in the test is therefore a no-op(
onChangeModeearly-returns whenmode === draft.mode) and fires noPUT. The only appearance PUT in the test is the one from clicking
"Light default" (
onSelectSingle("light")→theme_preference: "light").expectLightThemeClasses(page)passes purely from theoptimistic cache. It then calls
page.goto("/")almost immediately(~20 ms after the click). If PUT chore: Initial database scaffolding #2 (the light one) has not persisted
server-side, the new document loads the still-persisted
darkpreferencefrom embedded metadata, and every retry of the post-navigation assertion
sees
darkfor the full 10 s window.toPasscannot help post-navigation because it only re-reads stale, alreadypersisted state; it cannot make an unfinished/queued PUT complete.
Implemented fix: wait for the saving spinner (UI signal) before navigation
The repo's non-flaky settings tests wait for a visible save confirmation
(e.g. "settings updated successfully" toasts) before trusting the result. The
appearance theme form has no toast; its only save-in-progress feedback is the
<Spinner>. So the fix mirrors that pattern using the spinner:labelpropon
Spinner(setsrole="status"+aria-labelonly when provided;decorative otherwise).
Loadermoves its label onto its ownstatuscontainer so it keeps a single status region and its
getByLabelTextqueries keep working.
class, then wait for that spinner to be hidden before
page.goto("/").Why this is correct and idempotent:
isPendingbeforeonMutateapplies the optimistic cacheupdate, so by the time the optimistic light class is visible the spinner is
already showing if a save started. Waiting for it to clear guarantees the PUT
settled (and was not canceled by navigation) before the reload.
change, no PUT fires, the spinner never shows, and
toBeHiddenpassesimmediately. The reload still shows light.
Implemented changes
Spinnergains an opt-inlabelprop(
site/src/components/Spinner/Spinner.tsx):Loadercarries the label on its own status container(
site/src/components/Loader/Loader.tsx), and the appearance save spinners usethe new prop (
AppearanceForm.tsx):site/e2e/tests/users/userSettings.spec.ts:Validation:
biome check,tsc -p .pass; Loader + AppearancePage unit testsand the affected storybook tests pass; the e2e test passed 20/20 under
pnpm playwright:test -g "adjust user theme preference" --repeat-each 20(idempotent).
Alternatives considered
PUTvia awaitForApiCallhelper: rejected. Itcouples the test to a state transition and is non-idempotent, a repeat run
that is already light fires no PUT so the wait times out (14/15 repeats
failed). CI retries reuse the same ephemeral server, so this is a real
hazard, not just a local-repeat artifact.
page.request.get(...): rejected. It calls theAPI directly and stops being a site test.
role="status"on the sharedSpinner: rejected.LoaderwrapsSpinnerin its ownstatusdiv, so a default would nest two statusregions and break
Loader.test.tsx. The opt-inlabelprop avoids this.the repo guidance against
time.Sleep-style timing hacks and is inherentlyracy.
DEVEX-94 ("Light Theme setting not respected until Appearance page opened")
tracks product-side persistence/embedding behavior; this task is scoped to
stabilizing the e2e test.
Files touched
site/src/components/Spinner/Spinner.tsx(new opt-inlabelprop).site/src/components/Loader/Loader.tsx(label on its own status region).site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx(use thelabelprop on both appearance save spinners).site/e2e/tests/users/userSettings.spec.ts(wait for the spinner).This PR was created by Coder Agents on behalf of @jeremyruppel.