diff --git a/@types/gecko.d.mts b/@types/gecko.d.mts index 6c0ababc..996ea85f 100644 --- a/@types/gecko.d.mts +++ b/@types/gecko.d.mts @@ -247,8 +247,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; 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]() { 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 { @@ -363,6 +366,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 ebbd960c..4a2915ae 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; @@ -316,15 +319,15 @@ add_task(async function test_tab_pin_toggle_excmd() { using tab = await GlideTestUtils.new_tab(KEY_TEST_FILE + "?i=1"); const initial_tab_count = gBrowser.tabs.length; 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"); 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"); }); @@ -334,7 +337,7 @@ add_task(async function test_tab_pin_toggle_keymap() { using tab = await GlideTestUtils.new_tab(KEY_TEST_FILE + "?i=1"); const initial_tab_count = gBrowser.tabs.length; 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"); @@ -342,10 +345,10 @@ add_task(async function test_tab_pin_toggle_keymap() { await keys(""); is(gBrowser.tabs.length, initial_tab_count, "Tab count should remain the same"); - 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 "); is(gBrowser.tabs.length, initial_tab_count, "Tab count should remain the same"); }); @@ -355,7 +358,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);