Skip to content

No E2E test covers a Freighter account switch mid-session — the dApp's behavior in this scenario is completely unverified #95

Description

@prodbycorne

Problem

e2e/farm.spec.ts and e2e/mocks/freighter.ts provide Playwright coverage for the connect → deposit → countdown → unlock flow with a single, fixed mocked Freighter account for the duration of each test. No E2E test simulates the mocked Freighter API returning a different address partway through a session (e.g. after connecting, before or during a deposit) — the exact scenario described in the wallet-session-hardening issue elsewhere in this batch. Given that issue proposes changing StellarWalletContext to detect and react to account switches, this E2E coverage is necessary both to specify the desired behavior precisely (what should the UI show? does it force a disconnect? does it refresh positions for the new account automatically?) and to prevent regressions once implemented.

Acceptance Criteria

  • Extend e2e/mocks/freighter.ts to support changing the mocked active address after the test has already 'connected', simulating a live account switch.
  • Add a new Playwright spec (or extend e2e/farm.spec.ts) that connects as account A, switches the mock to account B mid-session, and asserts the app either (a) detects the switch and refreshes all account-scoped UI (balance, positions, credits) for account B, or (b) clearly disconnects/prompts reconnection — whichever behavior is decided in the companion wallet-session issue — rather than silently continuing to display account A's stale data.
  • Add an assertion that no transaction can be signed/submitted using a mismatched cached address after the switch is detected.
  • Document the expected behavior in this spec's comments so it doubles as executable specification for the fix.

Relevant Files

  • e2e/farm.spec.ts — existing E2E flow this new scenario should extend or sit alongside
  • e2e/mocks/freighter.ts — the mock Freighter implementation that needs to support a mid-test address change
  • src/context/StellarWalletContext.tsx — the implementation whose account-switch behavior this E2E test specifies and protects

Additional Notes

Confirmed: StellarWalletContext never re-checks publicKey after initial connect

src/context/StellarWalletContext.tsx's publicKey state (L61) is only ever set in three places, all inside connect()/initial-mount logic: L112, L139, L143 (setPublicKey(access.address) / setPublicKey(addr.address)). There is no interval, no addEventListener for any Freighter account-change event, and no re-fetch of getAddress() triggered by anything other than the explicit connect() call. The only recurring/reactive re-check in the whole file is refreshNetworkDetails (L65-77), which is wired to visibilitychange (L170-177) and refreshes networkName only — it never touches publicKey. So today, if Freighter's active account changes while the dApp tab stays open and connected, publicKey in React state is permanently stale until the user manually disconnects/reconnects or reloads the page. This matches #67's framing exactly and confirms there is currently zero code path this E2E test could exercise to detect account switches — the E2E spec described in this issue is effectively also the acceptance test for #67's fix, not just documentation of a gap.

Why the existing mock can't express this today

e2e/mocks/freighter.ts's injectFreighterMock(page, pubKey) (L18-92) bakes pubKey into the addInitScript closure at page-load time (L19, passed as an argument to the injected function) and every REQUEST_PUBLIC_KEY/REQUEST_ACCESS response (L45-50) replies with that same captured pubKey for the lifetime of the page. There is no mechanism to mutate the active address after injection — the closure captures pubKey by value once, at page.addInitScript time, and Playwright's addInitScript runs before any page script, so there's no reference to the live value that a later page.evaluate() call could reach into and change (the switch statement's payload construction happens entirely inside the injected function's own scope, L33-71).

Making the address mutable requires exposing it as a real, external, mutable piece of page state — e.g. stash it on window.__freighterMockState = { pubKey } inside the injected script (L19-91) instead of using the closed-over parameter directly, then add an exported helper like switchFreighterMockAccount(page, newPubKey) that does page.evaluate((addr) => { (window as any).__freighterMockState.pubKey = addr; }, newPubKey). The REQUEST_PUBLIC_KEY/REQUEST_ACCESS cases (L45-50) would then read window.__freighterMockState.pubKey at response time rather than closing over the original pubKey parameter, so a switch mid-session is reflected on the next Freighter API call the app happens to make.

The harder question this issue rightly identifies: what triggers the app to notice

Real Freighter (per its public API) does not push account-change events to the page — there is no window.freighter.on('accountChanged', ...) in the current Freighter extension API used here (@stellar/freighter-api, referenced via the FreighterModule type alias at StellarWalletContext.tsx L15). Detecting a switch therefore requires the dApp to poll — e.g. re-call getAddress() on the same cadence/trigger as refreshNetworkDetails already uses (visibilitychange, L170-177), or on a fixed interval similar to useLeaderboard's REFRESH_MS pattern or useSorobanEvents's 5s poll. This is exactly the kind of "what's the actual mechanism" decision #67 needs to make, and it directly determines what "detects the switch" looks like in this E2E test — worth explicitly noting in the spec's comments (per this issue's own AC #4) which polling trigger is assumed, so the test doesn't silently start failing if #67 picks a different mechanism (e.g. only polls on visibilitychange, so a headless E2E test that never blurs/refocuses the tab would need an explicit page.evaluate(() => document.dispatchEvent(new Event('visibilitychange'))) step to simulate that trigger, or the test needs to simulate a real focus loss via a second page.bringToFront() on a second tab).

Concrete places account-scoped UI would need to refresh once a switch is detected

Grep for publicKey consumers gives the surface area this E2E test's assertions should touch: src/app/farm/page.tsx (position list scoped to publicKey, L95/432), PoolDetailClient.tsx (L97, deposit/unlock eligibility), any leaderboard "your rank" computation (useLeaderboard's connectedRank, src/hooks/useLeaderboard.ts L71-75, which takes publicKey as a parameter) — a switch that isn't detected would leave the leaderboard showing account A's rank highlighted/labeled as "you" while actually connected as account B, a subtle but real correctness bug beyond the farm page alone. Worth broadening the new E2E spec's assertions beyond just farm-page balance/position to also check the leaderboard page if it's reachable in the same session, or at minimum flagging it as follow-up scope.

Testing strategy

  • Extend e2e/mocks/freighter.ts per the sketch above; keep TEST_PUBLIC_KEY/TEST_ADDRESS_DISPLAY (L3-7) as "account A" and add a second constant, e.g. TEST_PUBLIC_KEY_B/TEST_ADDRESS_DISPLAY_B, for the switched-to account, following the existing naming convention.
  • New spec: either extend e2e/farm.spec.ts (which already has the full RPC-mocking scaffolding — mockSorobanRpc, XDR fixtures at the top of the file, L1-30 — reusable for account B's balance/position responses) or a new e2e/account-switch.spec.ts importing the shared mockSorobanRpc/mock-Freighter helpers to avoid duplicating the RPC mock setup.
  • Sequence: connect as account A (existing flow), perform some account-scoped action or just observe account A's state rendered, call the new switchFreighterMockAccount(page, TEST_PUBLIC_KEY_B), trigger whatever detection mechanism StellarWalletContext never detects a Freighter account switch while connected, leaving publicKey stale #67 implements (poll interval elapse via page.clock/Playwright's clock API if the app uses real timers, or a simulated visibilitychange), then assert either: (a) account-scoped UI now reflects account B (balance/position refetched — assert via the mocked RPC call count/args, not just DOM text, to prove a real refetch happened rather than a coincidental stale render), or (b) the app disconnects/prompts reconnection — whichever StellarWalletContext never detects a Freighter account switch while connected, leaving publicKey stale #67 decides.
  • Critical negative assertion (AC Create Dynamic Leaderboard Showing Top Stakers and Credit Earners #3): after the switch, attempt to trigger a sign/submit flow (e.g. click Unlock) and assert the transaction request sent to the mocked RPC uses account B's address, never a cached account A address — this is the assertion most likely to catch a partial fix where UI text updates but an internal ref/closure still holds the old publicKey.
  • Confirm this spec is wired into the existing playwright script ("playwright": "playwright test --config e2e/playwright.config.ts", package.json) with no separate include list excluding new files — check e2e/playwright.config.ts's testMatch/testDir glob to be sure a new file (if choosing the new-file approach over extending farm.spec.ts) is actually picked up.

Cross-reference

Directly downstream of #67 ("StellarWalletContext never detects a Freighter account switch while connected, leaving publicKey stale") — this E2E issue cannot be meaningfully completed (beyond writing a currently-failing spec) until #67 decides the detection mechanism and implements it; the two should be treated as one deliverable for review purposes, with this issue's spec serving as #67's acceptance test. Also relevant: #70 ("lockAssets/unlockAssets have no guard against the wallet disconnecting between simulate and sign") is the same family of "wallet state can change out from under an in-flight operation" concern, and #93 (network-mismatch defense-in-depth) is the network-axis version of the same underlying problem this issue addresses on the account axis — a shared "verify wallet state immediately before signing" utility (see #93's addendum) would plausibly cover both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26testingUnit, integration, or e2e test coveragevery hardExtremely hard — deep expertise, careful design, and significant time requiredwalletFreighter wallet integration, session, and network switching

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions