Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions background_scripts/all_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions background_scripts/tab_operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
23 changes: 22 additions & 1 deletion content_scripts/link_hints.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
},
Expand Down Expand Up @@ -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,
)
) {
Expand Down
3 changes: 3 additions & 0 deletions content_scripts/mode_normal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
2 changes: 1 addition & 1 deletion test_harnesses/page_with_links.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

<br>
<br>
<div id="div-a" onclick="alert('hi')">div with an onclick attribute</div>
<div id="div-a" onclick='alert("hi")'>div with an onclick attribute</div>

<br>
<br>
Expand Down
11 changes: 11 additions & 0 deletions tests/unit_tests/commands_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
37 changes: 37 additions & 0 deletions tests/unit_tests/link_hints_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions tests/unit_tests/tab_operations_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});