Bugfix FXIOS-13351 [Widgets] Wait for tab restoration before closing private tabs from the widget#34569
Conversation
…private tabs from widget action
💪 Quality guardian2 tests files modified. You're a champion of test coverage! 🚀 🧹 Tidy commitJust 3 file(s) touched. Thanks for keeping it clean and review-friendly! 💬 Description craftsmanGreat PR description! Reviewers salute you 🫡 🦊 BrowserViewController CheckWe’re tracking the size of
✅ Code coverage
Client.app: Coverage: 43.55
Generated by 🚫 Danger Swift against 8b66767 |
| } | ||
|
|
||
| func closeAllPrivateTabs() { | ||
| // Avoid race condition; if we're restoring tabs, wait to close private tabs until completed. [FXIOS-13351] |
There was a problem hiding this comment.
Does this bug happens if you have the feature flag "Deeplink Optimization Refactor" turned ON? Does it happen only when it's OFF? I would confirm this before making any changes. If the refactor work fixes the problem, then let's not add a new AppEventQueue.wait.
|
@lmarceau (replying here — I can't reply inline on the review thread from my tooling)
Good question — I traced both flag states before making the change, and it's the opposite: the bug only reproduces when "Deeplink Optimization Refactor" is ON. With it OFF the legacy path is unaffected. Flag OFF (legacy, current release): Flag ON (refactor): the check is only This also matches the channel matrix QA reported in Jira (reproduces on main/beta, prod fine): The refactor path already handles this exact race for the neighbouring deeplinks with the same pattern: Happy to move the wait up into the coordinator's readiness check for this route instead, if you'd prefer it there. |
📜 Tickets
Jira ticket
Github issue
💡 Description
Root cause. The widget's Close Private Tabs action arrives as the
.action(.closePrivateTabs)route and ends inBrowserViewController.closeAllPrivateTabs(), which removes the tabs currently intabManager.privateTabs. With the deeplink-optimization refactor enabled,BrowserCoordinator.checkBrowserIsReady()no longer waits for!tabManager.isRestoringTabs(the legacy gate did). So on a cold launch (app force-closed → widget tapped) the route is handled while tab restoration is still in flight:removeTabsruns against the not-yet-restored (empty) in-memory list, andapplyRestorationResultthen merges the restored private tabs back — they survive. This matches the QA notes exactly: reproducible only when the app was force closed (cold start + restore race), not when backgrounded (tabs already in memory), and not in prod (legacy readiness gate waits for restore).The refactor's snapshot-and-merge restore explicitly supports additive pre-restore deeplinks (deeplink tabs land last), but a destructive pre-restore action wasn't accounted for.
Fix.
closeAllPrivateTabs()now defers itself until restoration completes whentabManager.isRestoringTabsis true, usingAppEventQueue.wait(for: .tabRestoration(windowUUID))— the same patternswitchToTabForURLOrOpenuses for the equivalent race (FXIOS-10916). No behavior change on the legacy path, where the readiness gate already guarantees restore has finished before the route is handled.Tests. Two new tests in
BrowserViewControllerTests: the not-restoring path removes private tabs immediately, and the restoring path performs no removal until.tabRestorationcompletes (deterministic —EventQueue.startedre-arms the event, and the queue executes inline on the main thread).MockTabManager.removeTabsnow records its calls to support these. The existing route-level test (BrowserCoordinatorTests,.action(.closePrivateTabs)→WindowManager) is unaffected and still passes that seam.Verification ask. I could not run the app or the unit tests locally (machine is on Xcode 26.0.1; the repo requires 26.5, and current
maindoes not compile on 26.0.1), so CI is the authoritative test run. Since the reproduction needs a real device (force close → tap widget's Close Private Tabs → reopen), it would be great if QA or the reference person could verify the scenario on this branch.📝 Checklist