Open Bug 2066788 Opened 23 days ago Updated 6 days ago

Incorrect handling of hidden split view in tab group when executing `replaceTabWithWindow()`

Categories

(Firefox :: Tabbed Browser: Split View, defect)

Firefox 156
defect

Tracking

()

Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox-esr153 --- unaffected
firefox155 --- unaffected
firefox156 --- wontfix
firefox157 --- wontfix
firefox158 --- fix-optional

People

(Reporter: mercichao, Assigned: sthompson)

References

(Regression)

Details

(Keywords: regression, Whiteboard: [fidefe-splitview])

Attachments

(1 file)

Attached video 2026_08_26_21_18_07.mp4

When hiding a split view inside a tab group and then executing replaceTabWithWindow(), the hidden state is not handled correctly. Depending on the scenario, the split view either becomes non‑hidden or turns into a ghost tab in the new window.


Steps to Reproduce (Scenario A)

  1. Open a new window with 3 tabs.
  2. Use two tabs to create a split view, and group it with the third tab.
  3. Select the third tab.
  4. Execute the following code in the console to hide the split view:
    gBrowser.hideTab(gBrowser.splitViews[0].tabs[0])
  1. Drag the group out of the window.
  2. Observe that the split view becomes non‑hidden in the new window.

Steps to Reproduce (Scenario B)

  1. Open a new window with 3 tabs.
  2. Use two tabs to create a split view, and group it with the third tab.
  3. Select the third tab.
  4. Execute the following code in the console to hide the split view:
    gBrowser.hideTab(gBrowser.splitViews[0].tabs[0])
  1. Press Ctrl+Shift+W to close the window.
  2. Press Ctrl+Shift+N to restore the window.
  3. Drag the tab group out of the window.
  4. Observe that the split view becomes a ghost tab in the new window.
See Also: → 2063789

Both scenarios come from browser/components/tabbrowser/Tabbrowser.sys.mjs adoptSplitView(), which rebuilds the split view in the destination window carrying over only splitViewId and never reapplies the hidden state.

Nothing else re-derives it: adoptTab() explicitly forwards pinned and usercontextid but not hidden (Tabbrowser.sys.mjs), the attribute-transfer block in swapBrowsersAndCloseOther() has no hidden case (Tabbrowser.sys.mjs), and since bug 2063789 the wrapper no longer recomputes hidden in connectedCallback (tabsplitview.js) — that removed line was previously papering over this.

  • Scenario A (live browsers): no hidden is copied at any step, so tabs and the fresh wrapper both come out visible.
  • Scenario B (pending tabs after restore): swapBrowsersAndCloseOther's isPending branch runs SessionStore.setTabState#restoreTabhideTab(newTab), but that happens inside the adoption loop, before addTabSplitView runs at line 4381. The tab has no .splitview yet, so the wrapper toggle at Tabbrowser.sys.mjs is skipped — hidden tabs inside a visible wrapper, i.e. the ghost tab.

Proposed fix, in adoptSplitView(): capture container.hidden || container.tabs.some(t => t.hidden) before the adoption loop, and after addTabSplitView() returns, re-hide — hideTab() each new tab (a no-op for the already-hidden Scenario B tabs, and it now propagates to the wrapper since .splitview is set), then toggleAttribute("hidden", true) on the returned wrapper explicitly so Scenario B's early-return in hideTab can't leave it visible. Doing it after addTabSplitView is what makes one fix cover both paths. Note hideTab refuses a selected tab, so the existing active-sibling exception still applies.

Worth fixing alongside: the same derivation exists in the ungrouped arm of createTabsForSessionRestore (Tabbrowser.sys.mjs) but is missing from the grouped arm (Tabbrowser.sys.mjs), so a hidden split view in a group relies entirely on a later #restoreTabhideTab.

Closest existing coverage is browser_replacewithwindow_splitview.js and browser_drag_splitview.js for adoption, and browser_tab_splitview.js plus test_splitview_hidden.py for hiding — but nothing combines hidden + split view + group + cross-window adoption, so this needs a new case.

Note I reasoned from the code and did not build or run this.


Suggested severity: S3
Leaves a corrupted tab strip (a ghost/empty split view) and silently loses hidden state, but reaching it requires tab hiding — only available to extensions or via the console — combined with a split view in a group dragged to a new window.


If you'd like to provide feedback on this comment, please use the 👍 or 👎 reaction.
If you want to categorize your feedback you can add one of the following tags: ai-triage-wrong-file, ai-triage-wrong-cause, ai-triage-hallucination, ai-triage-out-of-scope.

:sthompson, since you are the author of the regressor, bug 2063789, could you take a look? Also, could you set the severity field?

For more information, please visit BugBot documentation.

Flags: needinfo?(sthompson)

Scenario A is bug 2067258, which affects any hidden, loaded tab that moves to a different window. It will end up un-hidden in the destination window. I didn't find any other bugs mentioning this, which surprised me...

Scenario B ("ghost tab"):

  • the individual tabs in the split view keep their [hidden] attributes as expected because they were re-hidden by SessionStore in the new window
  • the split view wrapper itself does not get [hidden], diagnosis on comment 1 is correct that bug 2063789 did not account for this

I also noticed that adopting a hidden split view directly had strange behavior, but these might be separate bugs:

  • adopting an ungrouped hidden split view of loaded tabs with gBrowser.replaceTabsWithWindow() results in a new window with the split view + tabs visible, but the tab browsers don't appear loaded. Hitting the reload button results in Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsISHistory.reload] and the tabs don't load.
  • adopting an ungrouped hidden split view of unloaded tabs with gBrowser.replaceTabsWithWindow() results in a new window being created and then immediately, automatically closed. SessionStore reports that the window has 3 tabs when it closes. Reopening the closed window shows a visible new tab and a hidden split view containing two hidden tabs. I'm guessing that we might be hitting a check somewhere that's closing the window because it decides there are no visible tabs in the new window.

When doing this on a single hidden tab instead of a single hidden split view, the tab ended up loaded, operable, and visible as the active tab in the new window. This makes sense given bug 2067258, though we may decide to do something different in the future.

In general, I think we need to decide on specific desired behaviors for hidden split views instead of trying to address bugs in isolation. We're having a conversation on bug 2063792.

I think that making a fix to adoptSplitView may be simple and may not have negative side effects.

I can also review the changes I made to createTabsForSessionRestore for hidden split views, but I believe that it's restoring them fine. I can add to https://searchfox.org/firefox-main/rev/543bce9ee58f21e87c5fba0e1865d3b3c6aa27bd/browser/components/sessionstore/test/marionette/test_splitview_hidden.py in order to make sure.

Severity: -- → S3
Flags: needinfo?(sthompson)
See Also: → 2063792, 2067258
Whiteboard: [fidefe-splitview]

The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.

Status: UNCONFIRMED → NEW
Ever confirmed: true

I'll try to do a limited fix this week for uplift to 156 Beta this week.

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: