From 26af39ce872b1ea31ceea6b2f4734345edc78f73 Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Wed, 15 Apr 2026 23:27:58 +0530 Subject: [PATCH 1/4] chore(internal/types): replace any with actual type for BrowserTab --- @types/gecko.d.mts | 145 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 2 deletions(-) diff --git a/@types/gecko.d.mts b/@types/gecko.d.mts index e5e5060b..e450163b 100644 --- a/@types/gecko.d.mts +++ b/@types/gecko.d.mts @@ -251,8 +251,149 @@ interface nsIWindowMediator { declare var MozElements: MozElements; -// TODO(glide-types) -declare type BrowserTab = any; +interface BrowserTab extends MozTabbrowserTab { + _glide_commandline: GlideCommandLine | null; +} +// Source browser/components/tabbrowser/content/tab.js +interface MozTabbrowserTab extends XULElement { + // Source toolkit/content/customElements.js + disabled: boolean; + tabIndex: number; + // Source toolkit/content/customElements.js + label: string; + image: string; + command: string; + accessKey: string; + // Source toolkit/content/widgets/tabbox.js + value: string; + container: MozTabbrowserTabs; + control: MozTabbrowserTabs; + selected: boolean; + visible: boolean; + linkedPanel: string; + // Source browser/components/tabbrowser/content/tab.js + closing: boolean; + pinned: boolean; + isOpen: boolean; + readonly hidden: boolean; + muted: boolean; + multiselected: boolean; + userContextId: number; + soundPlaying: boolean; + pictureinpicture: boolean; + activeMediaBlocked: boolean; + undiscardable: boolean; + animationsEnabled: boolean; + isEmpty: boolean; + lastAccessed: number; + lastSeenActive: number; + attention: boolean; + owner: MozTabbrowserTab | null; + elementIndex: number; + group: MozTabbrowserTabGroup | null; + splitview: GlobalBrowser.MozTabSplitViewWrapper | null; + hasTabNote: boolean; + muteReason: string | undefined | null; + + readonly overlayIcon: Element | null; + readonly audioButton: Element | null; + readonly throbber: Element | null; + readonly iconImage: Element | null; + readonly sharingIcon: Element | null; + readonly textLabel: Element | null; + readonly closeButton: Element | null; + readonly noteIcon: Element | null; + readonly noteIconOverlay: Element | null; + + initialize(): void; + updateLastAccessed(date?: number): void; + updateLastSeenActive(): void; + updateLastUnloadedByTabUnloader(): void; + recordTimeFromUnloadToReload(): void; + toggleMuteAudio(muteReason?: string | null): void; + resumeDelayedMedia(): void; + setUserContextId(userContextId: number): void; + updateA11yDescription(): void; + updateSplitViewAriaLabel(index: number): void; + // Dynamically set by tabbrowser.js + linkedBrowser: GlobalBrowser.GlobalBrowser | null; + permanentKey: object | undefined; + description: string | undefined; + successor: MozTabbrowserTab | null; + predecessors: Set; + initializingTab: boolean | undefined; + openerTab: MozTabbrowserTab | undefined; + addedByAdoption: boolean | undefined; + removedByAdoption: boolean | undefined; + collapsed: boolean; +} + +// Source browser/components/tabbrowser/content/tabgroup.js +interface MozTabbrowserTabGroup extends XULElement { + overflowContainer: XULElement; + saveOnWindowClose: boolean; + + color: string; + defaultGroupName: string; + id: string; + hasActiveTab: boolean; + label: string; + name: string; + collapsed: boolean; + lastSeenActive: number; + tabs: MozTabbrowserTab[]; + tabsAndSplitViews: (MozTabbrowserTab | GlobalBrowser.MozTabSplitViewWrapper)[]; + labelElement: Element; + labelContainerElement: XULElement; + overflowCountLabel: Element; + wasCreatedByAdoption: boolean; + isBeingDragged: boolean; + hoverPreviewPanelActive: boolean; + + addTabs( + tabsOrSplitViews: (MozTabbrowserTab | GlobalBrowser.MozTabSplitViewWrapper)[], + metricsContext?: object, + ): void; + ungroupTabs(metricsContext?: object): void; + save(options?: object): void; + saveAndClose(options?: object): void; + select(): void; + isTabVisibleInGroup(tab: MozTabbrowserTab): boolean; +} + +// Source browser/components/tabbrowser/content/tabs.js +interface MozTabbrowserTabs extends XULElement { + startupTime: number; + arrowScrollbox: Element; + pinnedTabsContainer: Element; + previewPanel: object | null; + tooltip: string; + tabDragAndDrop: object; + emptyTabTitle: string; + tabbox: XULElement; + newTabButton: Element; + verticalMode: boolean; + expandOnHover: boolean; + overflowing: boolean; + allTabs: MozTabbrowserTab[]; + allGroups: MozTabbrowserTabGroup[]; + allSplitViews: MozTabSplitViewWrapper[]; + openTabs: MozTabbrowserTab[]; + nonHiddenTabs: MozTabbrowserTab[]; + visibleTabs: MozTabbrowserTab[]; + tablistHasFocus: boolean; + ariaFocusableItems: Element[]; + dragAndDropElements: Element[]; + init(): void; + destroy(): void; + advanceSelectedItem(direction: number, wrap: boolean): void; + ensureTabPreviewPanelLoaded(): Promise; + updateTabSoundLabel(tab: MozTabbrowserTab): void; + getRelatedElement(tab: MozTabbrowserTab): Element; + isContainerVerticalPinnedGrid(tab: MozTabbrowserTab): boolean; + cancelTabGroupPreview(): void; + showTabGroupPreview(group: MozTabbrowserTabGroup): void; +} declare var GlideBrowser: typeof import("../src/glide/browser/base/content/browser.mts").GlideBrowser; From 861d87666182fbac592c41f9146162239d27eb14 Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Thu, 16 Apr 2026 00:00:52 +0530 Subject: [PATCH 2/4] lint fixes --- @types/gecko.d.mts | 2 + .../base/content/browser-commandline.mts | 2 +- .../browser/base/content/browser-excmds.mts | 8 +++- src/glide/browser/base/content/browser.mts | 18 ++++++++- .../content/test/config/browser_options.ts | 3 ++ .../content/test/excmds/browser_excmds.ts | 37 ++++++++++--------- .../base/content/test/hints/browser_hints.ts | 4 +- .../test/navigation/browser_jumplist.ts | 20 ++++++++-- 8 files changed, 67 insertions(+), 27 deletions(-) diff --git a/@types/gecko.d.mts b/@types/gecko.d.mts index e450163b..63f7accb 100644 --- a/@types/gecko.d.mts +++ b/@types/gecko.d.mts @@ -253,6 +253,8 @@ declare var MozElements: MozElements; interface BrowserTab extends MozTabbrowserTab { _glide_commandline: GlideCommandLine | null; + [Symbol.dispose](): void; + [Symbol.asyncDispose](): Promise; } // Source browser/components/tabbrowser/content/tab.js interface MozTabbrowserTab extends XULElement { diff --git a/src/glide/browser/base/content/browser-commandline.mts b/src/glide/browser/base/content/browser-commandline.mts index f58f33ad..94100cc9 100644 --- a/src/glide/browser/base/content/browser-commandline.mts +++ b/src/glide/browser/base/content/browser-commandline.mts @@ -210,7 +210,7 @@ export class TabsCompletionSource implements GlideCompletionSource= all_tabs.length) { next_index = 0; @@ -349,6 +349,9 @@ class GlideExcmdsClass { case "tab_pin_toggle": { const tab = gBrowser.selectedTab; + if (!tab) { + throw new Error("No tab to pin/unpin"); + } if (tab.pinned) { gBrowser.unpinTab(tab); } else { @@ -362,6 +365,9 @@ class GlideExcmdsClass { } case "tab_duplicate": { + if (!gBrowser.selectedTab) { + throw new Error("No tab to duplicate"); + } gBrowser.duplicateTab(gBrowser.selectedTab, undefined, { inBackground: false }); break; } diff --git a/src/glide/browser/base/content/browser.mts b/src/glide/browser/base/content/browser.mts index 5ceadfb2..03d32c8b 100644 --- a/src/glide/browser/base/content/browser.mts +++ b/src/glide/browser/base/content/browser.mts @@ -893,7 +893,9 @@ class GlideBrowserClass { url: location.spec, get tab_id() { return assert_present( - GlideBrowser.extension.tabManager.getWrapper(gBrowser.selectedTab), + GlideBrowser.extension.tabManager.getWrapper( + assert_present(gBrowser.selectedTab, "could not resolve selected tab"), + ), "could not resolve tab wrapper", ).id; }, @@ -1899,6 +1901,9 @@ class GlideBrowserClass { */ async upsert_commandline(opts: GlideCommandLineShowOptions = {}) { const tab = gBrowser.selectedTab; + if (!tab) { + return null; + } const cached = this.#get_cached_commandline(tab); if (cached) { cached.show(opts); @@ -1931,6 +1936,9 @@ class GlideBrowserClass { } async toggle_commandline() { + if (!gBrowser.selectedTab) { + return; + } const commandline = this.#get_cached_commandline(gBrowser.selectedTab); if (!commandline) { await this.upsert_commandline(); @@ -1950,6 +1958,9 @@ class GlideBrowserClass { * This only returns anything **if** the commandline is open **and** it is focused. */ #get_active_commandline(): GlideCommandLine | null { + if (!gBrowser.selectedTab) { + return null; + } const commandline = this.#get_cached_commandline(gBrowser.selectedTab); if (!commandline) { return null; @@ -1971,6 +1982,9 @@ class GlideBrowserClass { } get_commandline(): GlideCommandLine | null { + if (!gBrowser.selectedTab) { + return null; + } return this.#get_cached_commandline(gBrowser.selectedTab); } @@ -1982,7 +1996,7 @@ class GlideBrowserClass { return tab._glide_commandline; } - #cache_commandline(tab: BrowserTab, excmdbar: Element): void { + #cache_commandline(tab: BrowserTab, excmdbar: GlideCommandLine): void { tab._glide_commandline = excmdbar; } } diff --git a/src/glide/browser/base/content/test/config/browser_options.ts b/src/glide/browser/base/content/test/config/browser_options.ts index 9b1dcac0..dda49558 100644 --- a/src/glide/browser/base/content/test/config/browser_options.ts +++ b/src/glide/browser/base/content/test/config/browser_options.ts @@ -26,6 +26,9 @@ async function newtab() { return { [Symbol.dispose]() { + if (!tab) { + return; + } gBrowser.removeTab(tab); }, }; diff --git a/src/glide/browser/base/content/test/excmds/browser_excmds.ts b/src/glide/browser/base/content/test/excmds/browser_excmds.ts index 9d988819..6346de1b 100644 --- a/src/glide/browser/base/content/test/excmds/browser_excmds.ts +++ b/src/glide/browser/base/content/test/excmds/browser_excmds.ts @@ -21,7 +21,10 @@ function current_url() { } add_task(async function test_tab_switching() { - const browser = gBrowser.tabContainer.allTabs.at(0).linkedBrowser; + const browser = gBrowser.tabContainer.allTabs.at(0)?.linkedBrowser; + if (!browser) { + throw new Error("No browser found"); + } BrowserTestUtils.startLoadingURIString(browser, INPUT_TEST_FILE + "?i=0"); await BrowserTestUtils.browserLoaded(browser); using _tab2 = await GlideTestUtils.new_tab(INPUT_TEST_FILE + "?i=1"); @@ -274,9 +277,9 @@ add_task(async function test_tab_pin() { using tab1 = await GlideTestUtils.new_tab(INPUT_TEST_FILE + "?i=1"); using _tab2 = await GlideTestUtils.new_tab(INPUT_TEST_FILE + "?i=2"); - is(gBrowser.selectedTab.pinned, false, "Current tab should not be pinned initially"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should not be pinned initially"); await keys(":tab_pin"); - is(gBrowser.selectedTab.pinned, true, "Current tab should be pinned after :tab_pin"); + is(gBrowser.selectedTab!.pinned, true, "Current tab should be pinned after :tab_pin"); const tab1_id = GlideBrowser.extension?.tabManager?.getWrapper?.(tab1)?.id; isnot(tab1_id, undefined, "Tab ID should be available"); @@ -294,11 +297,11 @@ add_task(async function test_tab_unpin() { using tab1 = await GlideTestUtils.new_tab(INPUT_TEST_FILE + "?i=1"); using _tab2 = await GlideTestUtils.new_tab(INPUT_TEST_FILE + "?i=2"); - gBrowser.pinTab(gBrowser.selectedTab); + gBrowser.pinTab(gBrowser.selectedTab!); - is(gBrowser.selectedTab.pinned, true, "Current tab should be pinned initially"); + is(gBrowser.selectedTab!.pinned, true, "Current tab should be pinned initially"); await keys(":tab_unpin"); - is(gBrowser.selectedTab.pinned, false, "Current tab should be unpinned after :tab_unpin"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should be unpinned after :tab_unpin"); gBrowser.pinTab(tab1); const tab1_id = GlideBrowser.extension?.tabManager?.getWrapper?.(tab1)?.id; @@ -315,19 +318,19 @@ add_task(async function test_tab_pin_toggle_excmd() { using tab = await GlideTestUtils.new_tab(KEY_TEST_FILE + "?i=1"); is(gBrowser.selectedTab, tab); - is(gBrowser.selectedTab.pinned, false, "Current tab should not be pinned initially"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should not be pinned initially"); await keys(":tab_pin_toggle"); if (gBrowser.selectedTab !== tab) { // idk man, Firefox seems to create an extra tab based off of the *other* tab that is active? - gBrowser.removeTab(gBrowser.selectedTab); - gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab!); + gBrowser.removeTab(gBrowser.selectedTab!); } - is(gBrowser.selectedTab.pinned, true, "Current tab should be pinned after :tab_pin_toggle"); + is(gBrowser.selectedTab!.pinned, true, "Current tab should be pinned after :tab_pin_toggle"); await keys(":tab_pin_toggle"); - is(gBrowser.selectedTab.pinned, false, "Current tab should be unpinned after :tab_pin_toggle"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should be unpinned after :tab_pin_toggle"); }); add_task(async function test_tab_pin_toggle_keymap() { @@ -335,7 +338,7 @@ add_task(async function test_tab_pin_toggle_keymap() { using tab = await GlideTestUtils.new_tab(KEY_TEST_FILE + "?i=1"); is(gBrowser.selectedTab, tab); - is(gBrowser.selectedTab.pinned, false, "Current tab should not be pinned initially"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should not be pinned initially"); await keys(""); await wait_for_mode("normal"); @@ -343,14 +346,14 @@ add_task(async function test_tab_pin_toggle_keymap() { await keys(""); if (gBrowser.selectedTab !== tab) { // idk man, Firefox seems to create an extra tab based off of the *other* tab that is active? - gBrowser.removeTab(gBrowser.selectedTab); - gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab!); + gBrowser.removeTab(gBrowser.selectedTab!); } - await waiter(() => gBrowser.selectedTab.pinned).is(true, "Tab should be pinned after "); + await waiter(() => gBrowser.selectedTab!.pinned).is(true, "Tab should be pinned after "); await keys(""); - await waiter(() => gBrowser.selectedTab.pinned).is(false, "Tab should be unpinned after "); + await waiter(() => gBrowser.selectedTab!.pinned).is(false, "Tab should be unpinned after "); }); add_task(async function test_tab_reopen() { @@ -359,7 +362,7 @@ add_task(async function test_tab_reopen() { const initial_tab_count = gBrowser.tabs.length; const test_url = INPUT_TEST_FILE + "?reopen_test"; - const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, test_url); + using tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, test_url); is(gBrowser.tabs.length, initial_tab_count + 1, "New tab should be created"); is(current_url(), test_url, "New tab should have the test URL"); diff --git a/src/glide/browser/base/content/test/hints/browser_hints.ts b/src/glide/browser/base/content/test/hints/browser_hints.ts index 07457ebd..7ff7c5a8 100644 --- a/src/glide/browser/base/content/test/hints/browser_hints.ts +++ b/src/glide/browser/base/content/test/hints/browser_hints.ts @@ -94,7 +94,7 @@ add_task(async function test_F_opens_new_tab() { is(GlideBrowser.state.mode, "normal", "Mode should return to 'normal' after following hint"); if (final_tab_count > initial_tab_count) { - gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab!); } }); }); @@ -438,7 +438,7 @@ add_task(async function test_numeric_hint_generator() { is(GlideBrowser.state.mode, "normal", "Mode should return to 'normal' after following hint"); if (final_tab_count > initial_tab_count) { - gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab!); } }); }); diff --git a/src/glide/browser/base/content/test/navigation/browser_jumplist.ts b/src/glide/browser/base/content/test/navigation/browser_jumplist.ts index 0429b316..62a066a1 100644 --- a/src/glide/browser/base/content/test/navigation/browser_jumplist.ts +++ b/src/glide/browser/base/content/test/navigation/browser_jumplist.ts @@ -18,7 +18,10 @@ function current_url() { } add_task(async function test_jumplist_basic_navigation() { - const browser = gBrowser.tabContainer.allTabs.at(0).linkedBrowser; + const browser = gBrowser.tabContainer.allTabs.at(0)?.linkedBrowser; + if (!browser) { + throw new Error("No browser found"); + } BrowserTestUtils.startLoadingURIString(browser, uri(0)); await BrowserTestUtils.browserLoaded(browser); @@ -47,7 +50,10 @@ add_task(async function test_jumplist_basic_navigation() { }); add_task(async function test_jumplist_prunes_forward_slice() { - const browser = gBrowser.tabContainer.allTabs.at(0).linkedBrowser; + const browser = gBrowser.tabContainer.allTabs.at(0)?.linkedBrowser; + if (!browser) { + throw new Error("No browser found"); + } BrowserTestUtils.startLoadingURIString(browser, uri(0)); await BrowserTestUtils.browserLoaded(browser); @@ -68,7 +74,10 @@ add_task(async function test_jumplist_prunes_forward_slice() { }); add_task(async function test_jumplist_max_entries_trim() { - const browser = gBrowser.tabContainer.allTabs.at(0).linkedBrowser; + const browser = gBrowser.tabContainer.allTabs.at(0)?.linkedBrowser; + if (!browser) { + throw new Error("No browser found"); + } BrowserTestUtils.startLoadingURIString(browser, uri(0)); await BrowserTestUtils.browserLoaded(browser); @@ -102,7 +111,10 @@ add_task(async function test_jumplist_max_entries_trim() { }); add_task(async function test_jumplist_deleted_intermediary_tab() { - const browser = gBrowser.tabContainer.allTabs.at(0).linkedBrowser; + const browser = gBrowser.tabContainer.allTabs.at(0)?.linkedBrowser; + if (!browser) { + throw new Error("No browser found"); + } BrowserTestUtils.startLoadingURIString(browser, uri(0)); await BrowserTestUtils.browserLoaded(browser); From e08dd0a9bfb61f5b783fccfe72e7aea8752ee03b Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Fri, 24 Apr 2026 10:12:38 +0530 Subject: [PATCH 3/4] refac: isolate disposable interface to `new_tab` test utility --- @types/gecko.d.mts | 2 -- src/glide/browser/base/content/GlideTestUtils.sys.mts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/@types/gecko.d.mts b/@types/gecko.d.mts index 63f7accb..e450163b 100644 --- a/@types/gecko.d.mts +++ b/@types/gecko.d.mts @@ -253,8 +253,6 @@ declare var MozElements: MozElements; interface BrowserTab extends MozTabbrowserTab { _glide_commandline: GlideCommandLine | null; - [Symbol.dispose](): void; - [Symbol.asyncDispose](): Promise; } // Source browser/components/tabbrowser/content/tab.js interface MozTabbrowserTab extends XULElement { diff --git a/src/glide/browser/base/content/GlideTestUtils.sys.mts b/src/glide/browser/base/content/GlideTestUtils.sys.mts index d47a942e..d38a499f 100644 --- a/src/glide/browser/base/content/GlideTestUtils.sys.mts +++ b/src/glide/browser/base/content/GlideTestUtils.sys.mts @@ -50,7 +50,7 @@ class GlideTestUtilsClass { * Behaves the same as `BrowserTestUtils.openNewForegroundTab()` but also defines * `[Symbol.dispose]()` so you can use it with `using`. */ - async new_tab(uri?: string): Promise { + async new_tab(uri?: string): Promise { const tab = await g.BrowserTestUtils.openNewForegroundTab(g.gBrowser, uri); return Object.assign(tab, { [Symbol.dispose]() { From 330a4d5bb4d8aa70a000809918448f6a660bc606 Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Tue, 19 May 2026 15:38:30 +0530 Subject: [PATCH 4/4] lint fix --- .../browser/base/content/test/excmds/browser_excmds.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/glide/browser/base/content/test/excmds/browser_excmds.ts b/src/glide/browser/base/content/test/excmds/browser_excmds.ts index 8de4911e..4a2915ae 100644 --- a/src/glide/browser/base/content/test/excmds/browser_excmds.ts +++ b/src/glide/browser/base/content/test/excmds/browser_excmds.ts @@ -324,10 +324,10 @@ add_task(async function test_tab_pin_toggle_excmd() { await keys(":tab_pin_toggle"); is(gBrowser.tabs.length, initial_tab_count, "Tab count should remain the same"); - is(gBrowser.selectedTab.pinned, true, "Current tab should be pinned after :tab_pin_toggle"); + is(gBrowser.selectedTab!.pinned, true, "Current tab should be pinned after :tab_pin_toggle"); await keys(":tab_pin_toggle"); - is(gBrowser.selectedTab.pinned, false, "Current tab should be unpinned after :tab_pin_toggle"); + is(gBrowser.selectedTab!.pinned, false, "Current tab should be unpinned after :tab_pin_toggle"); is(gBrowser.tabs.length, initial_tab_count, "Tab count should remain the same"); }); @@ -348,7 +348,7 @@ add_task(async function test_tab_pin_toggle_keymap() { await waiter(() => gBrowser.selectedTab!.pinned).is(true, "Tab should be pinned after "); await keys(""); - await waiter(() => gBrowser.selectedTab.pinned).is(false, "Tab should be unpinned after "); + await waiter(() => gBrowser.selectedTab!.pinned).is(false, "Tab should be unpinned after "); is(gBrowser.tabs.length, initial_tab_count, "Tab count should remain the same"); });