Skip to content

fix(frontend): stop the ErrorBoundary fallback from re-rendering AppShell - #1073

Merged
OlaGreat merged 2 commits into
OlaGreat:mainfrom
DSOTec:fix-1062-error-boundary-fallback
Aug 29, 2026
Merged

fix(frontend): stop the ErrorBoundary fallback from re-rendering AppShell#1073
OlaGreat merged 2 commits into
OlaGreat:mainfrom
DSOTec:fix-1062-error-boundary-fallback

Conversation

@DSOTec

@DSOTec DSOTec commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Problem

At its only call site the tree is:

<ErrorBoundary>
  <AppShell>
    …dashboard content…
  </AppShell>
</ErrorBoundary>

AppShell therefore renders inside the boundary, which makes it one of the components that can throw. But the fallback wrapped its error UI in AppShell again:

if (this.state.hasError) {
  return (
    <AppShell>
      …"Something went wrong"…
    </AppShell>
  );
}

So an error originating in AppShell itself — the nav, search, or wallet-connect render path — was caught, and then thrown a second time while rendering the fallback. That second throw has no ancestor boundary left to catch it, so React unmounts the whole tree: the page blanks entirely instead of degrading to "Something went wrong", which defeats the point of the boundary.

Solution

Render a plain, self-contained fallback: no AppShell, and no other app component. The AppShell import is dropped entirely so the fallback cannot regress into depending on app code again.

AppShell's outer chrome is inlined (min-h-screen px-6 py-8 sm:px-10 bg-ink dark:bg-black, plus the mx-auto max-w-6xl container) so the fallback still renders as a proper page rather than unstyled content on a bare background.

Errors thrown by page content — the common case — behave exactly as before.

Testing

Added frontend/src/components/error-boundary.test.tsx with 6 tests. AppShell is mocked behind a flag so a test can make it throw, which is the precise scenario this issue describes:

  1. children render when nothing throws
  2. the fallback renders when page content throws
  3. the boundary still degrades gracefully when AppShell itself is the error sourcerender() must not throw
  4. the fallback does not mount AppShell — asserted via a data-testid that must be absent
  5. the error message is surfaced in the details panel
  6. "Try again" resets and re-renders the children

Verified the tests actually catch the bug. Against the previous implementation, tests 3 and 4 fail with the error escaping the boundary:

× still degrades gracefully when AppShell itself is the error source
  → expected [Function] to not throw an error but 'Error: AppShell exploded' was thrown
× renders a fallback that does not mount AppShell
  → AppShell exploded

The other four pass both before and after, confirming the change is targeted and does not alter existing behaviour.

Full suite from a clean npm ci, matching what CI runs:

npm test          13 files, 109 tests passed   (also verified with CI=true)
npm run lint      0 errors
npm run build     OK
npx tsc --noEmit  clean
npx playwright test   2 passed
npx lhci autorun      exit 0 (warnings only, no failed assertions)

Edge cases considered

  • Styling drift: the fallback now duplicates AppShell's outer <main> classes. That duplication is deliberate — the whole point is that the fallback must not depend on AppShell — but it does mean a future change to the app background would need mirroring here. The alternative (extracting a shared layout primitive) would reintroduce a shared dependency in the fallback path, so I kept it inlined.
  • Loss of nav in the fallback: the fallback no longer shows the header nav. That is the intended trade-off; it already offers "Try again" and "Go home", which cover navigation without depending on the component that may have thrown.
  • Other call sites: ErrorBoundary is used in exactly one place (dashboard/[username]/page.tsx), so nothing else relied on the fallback providing AppShell chrome.

Note on the first commit

This branch carries two commits. The second is the actual fix for #1062 — a 9-line change to error-boundary.tsx plus its tests.

The first is the CI-pipeline repair already submitted as #1071, included only because main is currently red: Frontend CI, Lighthouse CI and E2E (Playwright) all fail on main, so a branch based on it cannot go green on its own. If #1071 merges first, I'll rebase and that commit disappears from this PR. Reviewing error-boundary.tsx and error-boundary.test.tsx alone gives the complete picture of this fix.

Closes #1062

DSOTec added 2 commits August 29, 2026 06:54
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.
…hell

At the dashboard call site the tree is <ErrorBoundary><AppShell>…</AppShell>
</ErrorBoundary>, so AppShell renders inside the boundary and is one of the
things that can throw. The fallback wrapped its error UI in AppShell again,
which meant an error originating in AppShell — the nav, search or
wallet-connect render path — threw a second time while rendering the
fallback. That throw had no ancestor boundary left to catch it, so the page
blanked entirely instead of degrading to "Something went wrong", defeating
the point of the boundary.

Render a plain, self-contained fallback instead: no AppShell and no other
app component, with the outer <main> chrome inlined so it still looks like a
page rather than unstyled content on a bare background.

Errors thrown by page content, which are the common case, are unaffected.

Adds tests covering both paths — content throwing and AppShell itself
throwing — plus the fallback's error details and the "Try again" reset. The
two AppShell-source tests fail against the previous implementation with the
error escaping the boundary.

Closes OlaGreat#1062
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@OlaGreat
OlaGreat merged commit 47d6a51 into OlaGreat:main Aug 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Frontend] Dashboard ErrorBoundary fallback re-renders the very AppShell that could have thrown

2 participants