diff --git a/background_scripts/all_commands.js b/background_scripts/all_commands.js index 607fb73a7..81de9fb38 100644 --- a/background_scripts/all_commands.js +++ b/background_scripts/all_commands.js @@ -215,6 +215,13 @@ const allCommands = [ advanced: true, }, + { + name: "LinkHints.activateModeToOpenInNewWindow", + desc: "Open a link in a new window", + group: "navigation", + advanced: true, + }, + { name: "LinkHints.activateModeToOpenIncognito", desc: "Open a link in incognito window", diff --git a/background_scripts/tab_operations.js b/background_scripts/tab_operations.js index 7e9ee933c..0723309a9 100644 --- a/background_scripts/tab_operations.js +++ b/background_scripts/tab_operations.js @@ -105,14 +105,14 @@ export async function openUrlInNewTab(request) { export async function openUrlInNewWindow(request) { const winConfig = { url: await UrlUtils.convertToUrl(request.url), - active: true, + focused: true, }; if (request.active != null) { - winConfig.active = request.active; + winConfig.focused = request.active; } - // Firefox does not support "about:newtab" in chrome.tabs.create, so omit it. - if (tabConfig["url"] === UrlUtils.chromeNewTabUrl) { - delete winConfig["url"]; + // Firefox does not support "about:newtab" in chrome.windows.create, so omit it. + if (winConfig.url === UrlUtils.chromeNewTabUrl) { + delete winConfig.url; } await chrome.windows.create(winConfig); } diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index 7b5d521c5..b5515f0cf 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -105,6 +105,17 @@ const COPY_LINK_URL = { } }, }; +const OPEN_IN_NEW_WINDOW = { + name: "new-window", + indicator: "Open link in new window", + linkActivator(link) { + if (link.href != null) { + chrome.runtime.sendMessage({ handler: "openUrlInNewWindow", url: link.href }); + } else { + DomUtils.simulateClick(link); + } + }, +}; const OPEN_INCOGNITO = { name: "incognito", indicator: "Open link in incognito window", @@ -152,6 +163,7 @@ const availableModes = [ OPEN_IN_NEW_FG_TAB, OPEN_WITH_QUEUE, COPY_LINK_URL, + OPEN_IN_NEW_WINDOW, OPEN_INCOGNITO, DOWNLOAD_LINK_URL, COPY_LINK_TEXT, @@ -351,6 +363,9 @@ const LinkHints = { activateModeToOpenIncognito(count) { this.activateMode(count, { mode: OPEN_INCOGNITO }); }, + activateModeToOpenInNewWindow(count) { + this.activateMode(count, { mode: OPEN_IN_NEW_WINDOW }); + }, activateModeToDownloadLink(count) { this.activateMode(count, { mode: DOWNLOAD_LINK_URL }); }, @@ -504,7 +519,13 @@ class LinkHintsMode { // NOTE(smblott) The modifier behaviour here applies only to alphabet hints. if ( ["Control", "Shift"].includes(event.key) && !Settings.get("filterLinkHints") && - [OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB].includes( + [ + OPEN_IN_CURRENT_TAB, + OPEN_WITH_QUEUE, + OPEN_IN_NEW_BG_TAB, + OPEN_IN_NEW_FG_TAB, + OPEN_IN_NEW_WINDOW, + ].includes( this.mode, ) ) { diff --git a/content_scripts/mode_normal.js b/content_scripts/mode_normal.js index eaa649c3f..7133b5632 100644 --- a/content_scripts/mode_normal.js +++ b/content_scripts/mode_normal.js @@ -351,6 +351,9 @@ const NormalModeCommands = { .bind(LinkHints), "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind(LinkHints), "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind(LinkHints), + "LinkHints.activateModeToOpenInNewWindow": LinkHints.activateModeToOpenInNewWindow.bind( + LinkHints, + ), "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind(LinkHints), "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind(LinkHints), diff --git a/test_harnesses/page_with_links.html b/test_harnesses/page_with_links.html index a9c914a68..adcc7df9a 100644 --- a/test_harnesses/page_with_links.html +++ b/test_harnesses/page_with_links.html @@ -55,7 +55,7 @@

-
div with an onclick attribute
+
div with an onclick attribute


diff --git a/tests/unit_tests/commands_test.js b/tests/unit_tests/commands_test.js index e3b63c11c..d918b1643 100644 --- a/tests/unit_tests/commands_test.js +++ b/tests/unit_tests/commands_test.js @@ -222,6 +222,17 @@ context("Validate commands and options data structures", () => { } }); + should("have LinkHints.activateModeToOpenInNewWindow command", () => { + const commandsByName = Utils.keyBy(allCommands, "name"); + const cmd = commandsByName["LinkHints.activateModeToOpenInNewWindow"]; + if (cmd == null) assert.fail("Command not found in allCommands"); + assert.equal("navigation", cmd.group); + // Verify it's also in NormalModeCommands. + if (globalThis.NormalModeCommands["LinkHints.activateModeToOpenInNewWindow"] == null) { + assert.fail("Command not found in NormalModeCommands"); + } + }); + should("have valid commands for each default key mapping", () => { const commandsByName = Utils.keyBy(allCommands, "name"); for (const [key, commandString] of Object.entries(defaultKeyMappings)) { diff --git a/tests/unit_tests/link_hints_test.js b/tests/unit_tests/link_hints_test.js index 4922689d9..e5eaa08e5 100644 --- a/tests/unit_tests/link_hints_test.js +++ b/tests/unit_tests/link_hints_test.js @@ -4,6 +4,43 @@ import "../../lib/settings.js"; import "../../content_scripts/mode.js"; import "../../content_scripts/link_hints.js"; +context("activateModeToOpenInNewWindow", () => { + setup(async () => { + await Settings.onLoaded(); + }); + + teardown(async () => { + await Settings.clear(); + }); + + should("activate link hints with open-in-new-window mode", () => { + let capturedMode = null; + stub(LinkHints, "activateMode", (count, { mode }) => { + capturedMode = mode; + }); + LinkHints.activateModeToOpenInNewWindow(3); + assert.equal("new-window", capturedMode.name); + assert.equal("Open link in new window", capturedMode.indicator); + }); + + should("linkActivator sends openUrlInNewWindow for links with href", () => { + let sentMessage = null; + stub(chrome.runtime, "sendMessage", (msg) => { + sentMessage = msg; + }); + // Capture the mode object via the stub, then exercise its linkActivator. + let capturedMode = null; + stub(LinkHints, "activateMode", (_count, { mode }) => { + capturedMode = mode; + }); + LinkHints.activateModeToOpenInNewWindow(1); + const mockLink = { href: "https://example.com" }; + capturedMode.linkActivator(mockLink); + assert.equal("openUrlInNewWindow", sentMessage.handler); + assert.equal("https://example.com", sentMessage.url); + }); +}); + context("With insufficient link characters", () => { setup(async () => { await Settings.onLoaded(); diff --git a/tests/unit_tests/tab_operations_test.js b/tests/unit_tests/tab_operations_test.js index 57366142a..578a36fb1 100644 --- a/tests/unit_tests/tab_operations_test.js +++ b/tests/unit_tests/tab_operations_test.js @@ -74,3 +74,27 @@ context("TabOperations openUrlInNewTab", () => { assert.equal(2, queryInfo.tabId); }); }); + +context("TabOperations openUrlInNewWindow", () => { + should("open a regular URL in a new window", async () => { + let windowConfig = null; + stub(chrome.windows, "create", (config) => { + windowConfig = config; + }); + const expected = "http://example.com"; + await to.openUrlInNewWindow({ url: expected }); + assert.equal(expected, windowConfig.url); + assert.isTrue(windowConfig.focused); + }); + + should("omit about:newtab URL in window config", async () => { + let windowConfig = null; + stub(chrome.windows, "create", (config) => { + windowConfig = config; + }); + await to.openUrlInNewWindow({ url: "about:newtab" }); + // about:newtab matches chromeNewTabUrl, so the url should be omitted. + // Before the fix, this threw ReferenceError: tabConfig is not defined. + assert.isFalse("url" in windowConfig); + }); +});