diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml
index aad8b0c741..d6f4b11399 100644
--- a/src/browser/base/content/zen-commands.inc.xhtml
+++ b/src/browser/base/content/zen-commands.inc.xhtml
@@ -55,6 +55,10 @@
+
+
+
+
diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js
index a93ba5b460..6107d11252 100644
--- a/src/browser/components/preferences/zen-settings.js
+++ b/src/browser/components/preferences/zen-settings.js
@@ -775,6 +775,10 @@ var zenMissingKeyboardShortcutL10n = {
key_wrToggleCaptureSequenceCmd: 'zen-key-wr-toggle-capture-sequence-cmd',
key_undoCloseWindow: 'zen-key-undo-close-window',
+ key_zenTabNext: 'zen-tab-next-shortcut',
+ key_zenTabPrevious: 'zen-tab-prev-shortcut',
+ key_toggleUnloadedCycling: 'zen-toggle-unloaded-cycling-shortcut',
+
'zen-glance-expand': 'zen-glance-expand',
key_selectTab1: 'zen-key-select-tab-1',
@@ -1160,4 +1164,9 @@ Preferences.addAll([
type: 'bool',
default: true,
},
+ {
+ id: 'zen.tabs.unloaded-navigation-mode',
+ type: 'string',
+ default: 'always',
+ },
]);
diff --git a/src/browser/components/preferences/zenTabsManagement.inc.xhtml b/src/browser/components/preferences/zenTabsManagement.inc.xhtml
index b8693566fd..32dc4525a1 100644
--- a/src/browser/components/preferences/zenTabsManagement.inc.xhtml
+++ b/src/browser/components/preferences/zenTabsManagement.inc.xhtml
@@ -68,6 +68,33 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/toolkit/content/widgets/tabbox-js.patch b/src/toolkit/content/widgets/tabbox-js.patch
index 72d25a5245..9bdf480f50 100644
--- a/src/toolkit/content/widgets/tabbox-js.patch
+++ b/src/toolkit/content/widgets/tabbox-js.patch
@@ -2,7 +2,15 @@ diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.
index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5eee090b258 100644
--- a/toolkit/content/widgets/tabbox.js
+++ b/toolkit/content/widgets/tabbox.js
-@@ -213,7 +213,7 @@
+@@ -125,6 +125,7 @@
+
+ const { ShortcutUtils } = imports;
+
++ return;
+ switch (ShortcutUtils.getSystemActionForEvent(event)) {
+ case ShortcutUtils.CYCLE_TABS:
+ Glean.browserUiInteraction.keyboard["ctrl-tab"].add(1);
+@@ -213,7 +214,7 @@
) {
this._inAsyncOperation = false;
if (oldPanel != this._selectedPanel) {
@@ -11,7 +19,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee
this._selectedPanel?.classList.add("deck-selected");
}
this.setAttribute("selectedIndex", val);
-@@ -610,7 +610,7 @@
+@@ -610,7 +611,7 @@
if (!tab) {
return;
}
@@ -20,7 +28,7 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee
if (otherTab != tab && otherTab.selected) {
otherTab._selected = false;
}
-@@ -823,7 +823,7 @@
+@@ -823,7 +824,7 @@
if (tab == startTab) {
return null;
}
@@ -29,7 +37,18 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee
return tab;
}
}
-@@ -888,7 +888,7 @@
+@@ -885,10 +886,18 @@
+ * @param {boolean} [aWrap]
+ */
+ advanceSelectedTab(aDir, aWrap) {
++
++ const basePref = Services.prefs.getStringPref('zen.tabs.unloaded-navigation-mode', 'never');
++ const invertedState = Services.prefs.getBoolPref('zen.tabs.unloaded-navigation-mode.inverted', false);
++ const includeUnloaded = (basePref === 'always' && !invertedState) || (basePref === 'never' && invertedState);
++ const tabFilter = includeUnloaded
++ ? (tab => tab.visible)
++ : (tab => tab.visible && !tab.hasAttribute('pending'));
++
let { ariaFocusedItem } = this;
let startTab = ariaFocusedItem;
if (!ariaFocusedItem || !this.allTabs.includes(ariaFocusedItem)) {
@@ -37,4 +56,23 @@ index 70afbfc87d543971e6f8a0661a44b682920a7bc4..2f767296db8043318fab2aeb39bfc5ee
+ startTab = gZenGlanceManager.getFocusedTab(aDir) || this.selectedItem;
}
let newTab = null;
-
+
+@@ -896,15 +905,15 @@
+ // which has a random placement in this.allTabs.
+ if (startTab.hidden) {
+ if (aDir == 1) {
+- newTab = this.allTabs.find(tab => tab.visible);
++ newTab = this.allTabs.find(tabFilter);
+ } else {
+- newTab = this.allTabs.findLast(tab => tab.visible);
++ newTab = this.allTabs.findLast(tabFilter);
+ }
+ } else {
+ newTab = this.findNextTab(startTab, {
+ direction: aDir,
+ wrap: aWrap,
+- filter: tab => tab.visible,
++ filter: tabFilter,
+ });
+ }
+
diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/ZenCommonUtils.mjs
index 5b0a09c67a..67663de9c8 100644
--- a/src/zen/common/ZenCommonUtils.mjs
+++ b/src/zen/common/ZenCommonUtils.mjs
@@ -17,7 +17,7 @@ window.gZenOperatingSystemCommonUtils = {
/* eslint-disable no-unused-vars */
class nsZenMultiWindowFeature {
- constructor() {}
+ constructor() { }
static get browsers() {
return Services.wm.getEnumerator('navigator:browser');
@@ -121,6 +121,31 @@ var gZenCommonActions = {
}
},
+ nextTab() {
+ gBrowser
+ .tabContainer
+ .advanceSelectedTab(1, true);
+ },
+
+ previousTab() {
+ gBrowser
+ .tabContainer
+ .advanceSelectedTab(-1, true);
+ },
+
+ toggleUnloadedCycling() {
+ try {
+ const currentMode = Services.prefs.getStringPref(
+ 'zen.tabs.unloaded-navigation-mode',
+ 'always'
+ );
+ const nextMode = currentMode === 'always' ? 'never' : 'always';
+ Services.prefs.setStringPref('zen.tabs.unloaded-navigation-mode', nextMode);
+ } catch (e) {
+ console.error('[gZenCommonActions] Error on unloaded cycling:', e);
+ }
+ },
+
throttle(f, delay) {
let timer = 0;
return function (...args) {
diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js
index 5d8316589c..e1b8e3c599 100644
--- a/src/zen/common/zen-sets.js
+++ b/src/zen/common/zen-sets.js
@@ -47,6 +47,15 @@ document.addEventListener(
case 'cmd_zenSplitViewUnsplit':
gZenViewSplitter.toggleShortcut('unsplit');
break;
+ case 'cmd_zenTabNext':
+ gZenCommonActions.nextTab();
+ break;
+ case 'cmd_zenTabPrev':
+ gZenCommonActions.previousTab();
+ break;
+ case 'cmd_zenToggleUnloadedCycling':
+ gZenCommonActions.toggleUnloadedCycling();
+ break;
case 'cmd_zenSplitViewContextMenu':
gZenViewSplitter.contextSplitTabs();
break;
diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs
index f9636a4ebb..77abeb53b4 100644
--- a/src/zen/kbs/ZenKeyboardShortcuts.mjs
+++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs
@@ -229,10 +229,10 @@ class nsKeyShortcutModifiers {
this.#control == other.#control &&
(AppConstants.platform == 'macosx'
? (this.#meta || this.#accel) == (other.#meta || other.#accel) &&
- this.#control == other.#control
+ this.#control == other.#control
: // In other platforms, we can have control and accel counting as the same thing
- this.#meta == other.#meta &&
- (this.#control || this.#accel) == (other.#control || other.#accel))
+ this.#meta == other.#meta &&
+ (this.#control || this.#accel) == (other.#control || other.#accel))
);
}
@@ -303,6 +303,7 @@ class KeyShortcut {
#reserved = false;
#internal = false;
#shouldBeEmpty = false;
+ #isPrecedent = false;
constructor(
id,
@@ -314,7 +315,8 @@ class KeyShortcut {
l10nId,
disabled = false,
reserved = false,
- internal = false
+ internal = false,
+ isPrecedent = false
) {
this.#id = id;
this.#key = key?.toLowerCase();
@@ -331,6 +333,7 @@ class KeyShortcut {
this.#disabled = disabled;
this.#reserved = reserved;
this.#internal = internal;
+ this.#isPrecedent = isPrecedent;
}
isEmpty() {
@@ -369,7 +372,8 @@ class KeyShortcut {
json['l10nId'],
json['disabled'],
json['reserved'],
- json['internal']
+ json['internal'],
+ json['isPrecedent']
);
}
@@ -379,16 +383,17 @@ class KeyShortcut {
key.getAttribute('key'),
key.getAttribute('keycode'),
group ??
- KeyShortcut.getGroupFromL10nId(
- KeyShortcut.sanitizeL10nId(key.getAttribute('data-l10n-id')),
- key.getAttribute('id')
- ),
+ KeyShortcut.getGroupFromL10nId(
+ KeyShortcut.sanitizeL10nId(key.getAttribute('data-l10n-id')),
+ key.getAttribute('id')
+ ),
nsKeyShortcutModifiers.parseFromXHTMLAttribute(key.getAttribute('modifiers')),
key.getAttribute('command'),
key.getAttribute('data-l10n-id'),
key.getAttribute('disabled') == 'true',
key.getAttribute('reserved') == 'true',
- key.getAttribute('internal') == 'true'
+ key.getAttribute('internal') == 'true',
+ key.getAttribute('isPrecedent') == 'true'
);
}
@@ -452,6 +457,9 @@ class KeyShortcut {
if (this.#internal) {
key.setAttribute('internal', this.#internal);
}
+ if (this.#isPrecedent) {
+ key.setAttribute('isPrecedent', this.#isPrecedent);
+ }
key.setAttribute('zen-keybind', 'true');
return key;
@@ -517,6 +525,10 @@ class KeyShortcut {
return this.#internal;
}
+ isPrecedent() {
+ return this.#isPrecedent;
+ }
+
isInvalid() {
return this.#key == '' && this.#keycode == '' && this.#l10nId == null;
}
@@ -540,6 +552,7 @@ class KeyShortcut {
disabled: this.#disabled,
reserved: this.#reserved,
internal: this.#internal,
+ isPrecedent: this.#isPrecedent,
};
}
@@ -1051,8 +1064,55 @@ class nsZenKeyboardShortcutsVersioner {
}
}
if (version < 10) {
- // Migrate from version 9 to 10
- // 1) Add the new pin/unpin tab toggle shortcut with Ctrl+Shift+D
+ // 1) Migrate from version 9 to 10
+ // In this new version, we add customizable shortcuts for switching to the next/previous tab and toggling unloaded tab cycling.
+ data.push(
+ new KeyShortcut(
+ 'zen-tab-next-shortcut',
+ 'TAB',
+ 'VK_TAB',
+ 'windowAndTabManagement',
+ nsKeyShortcutModifiers.fromObject({ accel: true }),
+ 'cmd_zenTabNext',
+ 'zen-tab-next-shortcut',
+ false, // disabled
+ false, // reserved
+ false, // internal
+ true // isPrecedent
+ )
+ );
+ data.push(
+ new KeyShortcut(
+ 'zen-tab-prev-shortcut',
+ 'TAB',
+ 'VK_TAB',
+ 'windowAndTabManagement',
+ nsKeyShortcutModifiers.fromObject({ accel: true, shift: true }),
+ 'cmd_zenTabPrev',
+ 'zen-tab-prev-shortcut',
+ false, // disabled
+ false, // reserved
+ false, // internal
+ true // isPrecedent
+ )
+ );
+ data.push(
+ new KeyShortcut(
+ 'zen-toggle-unloaded-cycling-shortcut',
+ '',
+ '',
+ 'windowAndTabManagement',
+ nsKeyShortcutModifiers.fromObject({}),
+ 'cmd_zenToggleUnloadedCycling',
+ 'zen-toggle-unloaded-cycling-shortcut',
+ false, // disabled
+ false, // reserved
+ false, // internal
+ true // isPrecedent
+ )
+ );
+
+ // 2) Add the new pin/unpin tab toggle shortcut with Ctrl+Shift+D
data.push(
new KeyShortcut(
'zen-toggle-pin-tab',
@@ -1065,7 +1125,7 @@ class nsZenKeyboardShortcutsVersioner {
)
);
- // 2) Add shortcut to expand Glance into a full tab: Default Accel+O
+ // 3) Add shortcut to expand Glance into a full tab: Default Accel+O
data.push(
new KeyShortcut(
'zen-glance-expand',
@@ -1123,10 +1183,8 @@ var gZenKeyboardShortcutsManager = {
async init() {
if (this.inBrowserView) {
const loadedShortcuts = await this._loadSaved();
-
- this._currentShortcutList = this.versioner.fixedKeyboardShortcuts(loadedShortcuts);
+ this._currentShortcutList = this.versioner.fixedKeyboardShortcuts(loadedShortcuts) || [];
this._applyShortcuts();
-
await this._saveShortcuts();
}
},
@@ -1231,6 +1289,46 @@ var gZenKeyboardShortcutsManager = {
this.triggerShortcutRebuild();
},
+ _registerPrecedentShortcut(shortcut, browser) {
+ const listener = (event) => {
+ let keyMatch = false;
+ if (shortcut.getKeyName()) {
+ keyMatch = event.key.toLowerCase() === shortcut.getKeyName().toLowerCase();
+ } else if (shortcut.getKeyCode()) {
+ for (const [mapKey, mapValue] of Object.entries(KEYCODE_MAP)) {
+ if (mapValue === shortcut.getKeyCode()) {
+ if (mapKey.toLowerCase() === event.code.toLowerCase()) {
+ keyMatch = true;
+ }
+ break;
+ }
+ }
+ }
+
+ if (!keyMatch) {
+ return;
+ }
+
+ const modifiers = shortcut.getModifiers();
+ const accelPressed = AppConstants.platform === 'macosx' ? event.metaKey : event.ctrlKey;
+ const modifiersMatch =
+ modifiers.accel === accelPressed &&
+ modifiers.alt === event.altKey &&
+ modifiers.shift === event.shiftKey;
+
+ if (modifiersMatch) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ const command = browser.document.getElementById(shortcut.getAction());
+ if (command) {
+ command.doCommand();
+ }
+ }
+ };
+
+ browser.addEventListener('keydown', listener, true);
+ },
+
_applyShortcuts() {
for (const browser of nsZenMultiWindowFeature.browsers) {
let mainKeyset = browser.document.getElementById(ZEN_MAIN_KEYSET_ID);
@@ -1251,8 +1349,13 @@ var gZenKeyboardShortcutsManager = {
if (key.isInternal()) {
continue;
}
- let child = key.toXHTMLElement(browser);
- keyset.appendChild(child);
+
+ if (key.isPrecedent()) {
+ this._registerPrecedentShortcut(key, browser);
+ } else {
+ let child = key.toXHTMLElement(browser);
+ keyset.appendChild(child);
+ }
}
this._applyDevtoolsShortcuts(browser);
diff --git a/src/zen/tests/tabs/browser.toml b/src/zen/tests/tabs/browser.toml
index 3d86041c67..e3a6fcc7f8 100644
--- a/src/zen/tests/tabs/browser.toml
+++ b/src/zen/tests/tabs/browser.toml
@@ -7,9 +7,10 @@ support-files = [
"head.js",
]
+["browser_tab_unloaded_navigation.js"]
["browser_tabs_empty_checks.js"]
["browser_drag_drop_vertical.js"]
tags = [
"drag-drop",
"vertical-tabs"
-]
\ No newline at end of file
+]
diff --git a/src/zen/tests/tabs/browser_tab_unloaded_navigation.js b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js
new file mode 100644
index 0000000000..93e4a109a1
--- /dev/null
+++ b/src/zen/tests/tabs/browser_tab_unloaded_navigation.js
@@ -0,0 +1,268 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+/**
+ * Tests tab navigation between loaded/unloaded tabs based on user preference
+ */
+
+const URL1 = 'http://example.com/page/1';
+const URL2 = 'http://example.com/page/2_unloaded';
+const URL3 = 'http://example.com/page/3';
+
+// Helper function to create a normal loaded tab
+async function createLoadedTab(url) {
+ info(`Creating loaded tab predictably with URL ${url}`);
+ const tab = BrowserTestUtils.addTab(gBrowser, url, {
+ inBackground: true,
+ });
+
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ return tab;
+}
+
+function createUnloadedTab(url) {
+ const tab = BrowserTestUtils.addTab(gBrowser, url, {
+ inBackground: true,
+ skipAnimation: true,
+ });
+
+ tab.linkedBrowser.setAttribute('pending', 'true');
+ tab.setAttribute('pending', 'true');
+
+ info(`New unloaded tab created at index ${gBrowser.tabs.indexOf(tab)} with URL ${url}`);
+ return tab;
+}
+
+function resetPreferences() {
+ Services.prefs.clearUserPref('zen.tabs.unloaded-navigation-mode');
+}
+
+async function waitForTabSelection(expectedTab) {
+ await BrowserTestUtils.waitForCondition(
+ () => gBrowser.selectedTab === expectedTab,
+ 'Waiting for tab to be selected'
+ );
+}
+
+add_setup(async () => {
+ resetPreferences();
+});
+
+add_task(async function test_basic_unloaded_navigation() {
+ info('Basic test to verify the test infrastructure works');
+
+ const tab1 = await createLoadedTab(URL1);
+ const tab2 = createUnloadedTab(URL2);
+
+ ok(tab1, 'Loaded tab should be created');
+ ok(tab2, 'Unloaded tab should be created');
+ is(tab2.hasAttribute('pending'), true, 'Tab should have pending attribute');
+ is(tab1.hasAttribute('pending'), false, 'Loaded tab should not have pending attribute');
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function test_unloaded_navigation_always_mode() {
+ info("Testing navigation with 'always' mode (includes unloaded tabs)");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [['zen.tabs.unloaded-navigation-mode', 'always']],
+ });
+
+ const navigateAndAssert = async (direction, expectedTab, message) => {
+ gBrowser.tabContainer.advanceSelectedTab(direction, false);
+ await TestUtils.waitForTick();
+ info(
+ `After navigating by ${direction}, selected tab is at index ${gBrowser.tabs.indexOf(gBrowser.selectedTab)}`
+ );
+ is(gBrowser.selectedTab, expectedTab, message);
+ };
+
+ const loadedTab = await createLoadedTab(URL1);
+ const unloadedTab = createUnloadedTab(URL2);
+ const initialIndex = gBrowser.tabs.indexOf(loadedTab);
+
+ gBrowser.selectedTab = loadedTab;
+ await waitForTabSelection(loadedTab);
+
+ await navigateAndAssert(
+ 1,
+ gBrowser.tabs[initialIndex + 1],
+ 'Should navigate forward to the next tab (unloaded)'
+ );
+
+ await navigateAndAssert(-1, loadedTab, 'Should navigate backward to the previous tab (loaded)');
+
+ BrowserTestUtils.removeTab(loadedTab);
+ BrowserTestUtils.removeTab(unloadedTab);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_unloaded_navigation_never_mode() {
+ info("Testing navigation with 'never' mode (skips unloaded tabs) using URL comparison");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [['zen.tabs.unloaded-navigation-mode', 'never']],
+ });
+
+ await TestUtils.waitForTick();
+
+ /**
+ * Helper to test navigation scenarios. Each run is isolated by creating and
+ * then cleaning up its own set of tabs.
+ */
+ async function runNavTest({ setup, startIndex, direction, shouldWrap, expectedURL, description }) {
+ info(`Running test: ${description}`);
+
+ let allTestTabs = [];
+ try {
+ // Create all the tabs required for this specific test scenario
+ for (const tabConfig of setup) {
+ let newTab;
+ if (tabConfig.type === 'loaded') {
+ newTab = await createLoadedTab(tabConfig.url);
+ } else {
+ newTab = createUnloadedTab(tabConfig.url);
+ }
+ allTestTabs.push(newTab);
+ }
+
+ // put tabs in ascending order for the tests to make sense
+ for (let i = 0; i < allTestTabs.length; i++) {
+ const targetIndex = gBrowser.tabs.length - allTestTabs.length + i;
+ gBrowser.moveTabTo(allTestTabs[i], targetIndex);
+ }
+
+ //trimming unnecessary about:blanks
+ for (let i = gBrowser.tabs.length - 1; i >= 0; i--) {
+ const tab = gBrowser.tabs[i];
+ if (
+ tab.linkedBrowser.currentURI.spec === 'about:blank' &&
+ tab.visible &&
+ !tab.hasAttribute('pending') &&
+ !allTestTabs.includes(tab)
+ ) {
+ gBrowser.removeTab(tab);
+ }
+ }
+
+ const startTab = allTestTabs[startIndex];
+ gBrowser.selectedTab = startTab;
+ await waitForTabSelection(startTab);
+
+ const startURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec;
+ const startTabIndex = gBrowser.tabs.indexOf(gBrowser.selectedTab);
+
+ info(`Starting navigation from tab index ${startTabIndex}, direction ${direction}`);
+
+ gBrowser.tabContainer.advanceSelectedTab(direction, shouldWrap);
+ await TestUtils.waitForTick();
+
+ const finalURL = gBrowser.selectedTab.linkedBrowser.currentURI.spec;
+ const finalTabIndex = gBrowser.tabs.indexOf(gBrowser.selectedTab);
+
+ info(
+ `--> Navigation result: Started on tab ${startTabIndex} [${startURL}], ended on tab ${finalTabIndex} [${finalURL}]`
+ );
+
+ is(finalURL, expectedURL, description);
+ } finally {
+ // just remove all tabs created for a specific test run before moving on to the next. Trying to 'KISS'
+ for (const tab of allTestTabs) {
+ if (tab && tab.isConnected) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+ }
+ }
+
+ // test scenarios
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'loaded', url: URL3 },
+ ],
+ startIndex: 0,
+ direction: 1,
+ shouldWrap: false,
+ expectedURL: URL3,
+ description: 'Should skip one unloaded tab and land on the correct URL.',
+ });
+
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'loaded', url: URL3 },
+ ],
+ startIndex: 2,
+ direction: -1,
+ shouldWrap: false,
+ expectedURL: URL1,
+ description: 'Should skip one unloaded tab backward and land on the correct URL.',
+ });
+
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'loaded', url: URL3 },
+ ],
+ startIndex: 0,
+ direction: 1,
+ shouldWrap: false,
+ expectedURL: URL3,
+ description: 'Should skip multiple unloaded tabs and land on the correct URL.',
+ });
+
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'unloaded', url: URL2 },
+ ],
+ startIndex: 0,
+ direction: 1,
+ shouldWrap: false,
+ expectedURL: URL1,
+ description: 'Should not move if there is no next loaded tab.',
+ });
+
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'unloaded', url: URL2 },
+ { type: 'loaded', url: URL3 },
+ ],
+ startIndex: 2,
+ direction: -1,
+ shouldWrap: false,
+ expectedURL: URL1,
+ description: "Should skip the unloaded tab and reach the correct URLto the first loaded tab's URL, by moving backwards.",
+ });
+
+ await runNavTest({
+ setup: [
+ { type: 'loaded', url: URL1 },
+ { type: 'loaded', url: URL2 },
+ { type: 'loaded', url: URL3 },
+ ],
+ startIndex: 2,
+ direction: 1,
+ shouldWrap: true,
+ expectedURL: URL1,
+ description: "Should wrap from the last tab to the first one, by moving forward.",
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+registerCleanupFunction(() => {
+ resetPreferences();
+});