aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CREDITS1
-rw-r--r--README.md2
-rw-r--r--background_scripts/commands.coffee4
-rw-r--r--background_scripts/completion.coffee26
-rw-r--r--background_scripts/main.coffee160
-rw-r--r--background_scripts/settings.coffee23
-rw-r--r--content_scripts/link_hints.coffee6
-rw-r--r--content_scripts/mode.coffee10
-rw-r--r--content_scripts/mode_visual_edit.coffee6
-rw-r--r--content_scripts/scroller.coffee3
-rw-r--r--content_scripts/ui_component.coffee15
-rw-r--r--content_scripts/vimium_frontend.coffee88
-rw-r--r--content_scripts/vomnibar.coffee5
-rw-r--r--lib/dom_utils.coffee50
-rw-r--r--manifest.json1
-rw-r--r--pages/help_dialog.html1
-rw-r--r--pages/options.coffee10
-rw-r--r--pages/options.html2
-rw-r--r--pages/vomnibar.coffee41
-rw-r--r--tests/dom_tests/dom_tests.coffee47
-rw-r--r--tests/unit_tests/completion_test.coffee8
-rw-r--r--tests/unit_tests/settings_test.coffee2
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee2
23 files changed, 317 insertions, 196 deletions
diff --git a/CREDITS b/CREDITS
index 60a5acaa..539b998c 100644
--- a/CREDITS
+++ b/CREDITS
@@ -42,5 +42,6 @@ Contributors:
Werner Laurensse (github: ab3)
Timo Sand <timo.j.sand@gmail.com> (github: deiga)
Shiyong Chen <billbill290@gmail.com> (github: UncleBill)
+ Utkarsh Upadhyay <musically.ut@gmail.com) (github: musically-ut)
Feel free to add real names in addition to GitHub usernames.
diff --git a/README.md b/README.md
index 36d176e6..96cde9bb 100644
--- a/README.md
+++ b/README.md
@@ -174,7 +174,7 @@ Release Notes
- Added `gU`, which goes to the root of the current URL.
- Added `yt`, which duplicates the current tab.
- Added `W`, which moves the current tab to a new window.
-- Added marks for saving and jumping to sections of a page. `mX` to set a mark and `X` to return to it.
+- Added marks for saving and jumping to sections of a page. `mX` to set a mark and `` `X`` to return to it.
- Added "LinkHints.activateModeToOpenIncognito", currently an advanced, unbound command.
- Disallowed repeat tab closings, since this causes trouble for many people.
- Update our Chrome APIs so Vimium works on Chrome 28+.
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index e371acf3..9aa90c45 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -289,8 +289,8 @@ commandDescriptions =
openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]
enterInsertMode: ["Enter insert mode", { noRepeat: true }]
- enterVisualMode: ["Enter visual mode", { noRepeat: true }]
- enterVisualLineMode: ["Enter visual line mode", { noRepeat: true }]
+ enterVisualMode: ["Enter visual mode (beta feature)", { noRepeat: true }]
+ enterVisualLineMode: ["Enter visual line mode (beta feature)", { noRepeat: true }]
# enterEditMode: ["Enter vim-like edit mode (not yet implemented)", { noRepeat: true }]
focusInput: ["Focus the first text box on the page. Cycle between them using tab",
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 177892fb..6a1c0d30 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -204,7 +204,7 @@ class DomainCompleter
domains: null
filter: (queryTerms, onComplete) ->
- return onComplete([]) if queryTerms.length > 1
+ return onComplete([]) unless queryTerms.length == 1
if @domains
@performSearch(queryTerms, onComplete)
else
@@ -344,11 +344,33 @@ class SearchEngineCompleter
computeRelevancy: -> 1
refresh: ->
- this.searchEngines = root.Settings.getSearchEngines()
+ @searchEngines = SearchEngineCompleter.getSearchEngines()
getSearchEngineMatches: (queryTerms) ->
(1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {}
+ # Static data and methods for parsing the configured search engines. We keep a cache of the search-engine
+ # mapping in @searchEnginesMap.
+ @searchEnginesMap: null
+
+ # Parse the custom search engines setting and cache it in SearchEngineCompleter.searchEnginesMap.
+ @parseSearchEngines: (searchEnginesText) ->
+ searchEnginesMap = SearchEngineCompleter.searchEnginesMap = {}
+ for line in searchEnginesText.split /\n/
+ tokens = line.trim().split /\s+/
+ continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#")
+ keywords = tokens[0].split ":"
+ continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ].
+ searchEnginesMap[keywords[0]] =
+ url: tokens[1]
+ description: tokens[2..].join(" ")
+
+ # Fetch the search-engine map, building it if necessary.
+ @getSearchEngines: ->
+ unless SearchEngineCompleter.searchEnginesMap?
+ SearchEngineCompleter.parseSearchEngines Settings.get "searchEngines"
+ SearchEngineCompleter.searchEnginesMap
+
# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top
# 10. Queries from the vomnibar frontend script come through a multi completer.
class MultiCompleter
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 9eafc2a2..72fe1092 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -1,7 +1,24 @@
root = exports ? window
-currentVersion = Utils.getCurrentVersion()
+# The browser may have tabs already open. We inject the content scripts immediately so that they work straight
+# away.
+chrome.runtime.onInstalled.addListener ({ reason }) ->
+ # See https://developer.chrome.com/extensions/runtime#event-onInstalled
+ return if reason in [ "chrome_update", "shared_module_update" ]
+ manifest = chrome.runtime.getManifest()
+ # Content scripts loaded on every page should be in the same group. We assume it is the first.
+ contentScripts = manifest.content_scripts[0]
+ jobs = [ [ chrome.tabs.executeScript, contentScripts.js ], [ chrome.tabs.insertCSS, contentScripts.css ] ]
+ # Chrome complains if we don't evaluate chrome.runtime.lastError on errors (and we get errors for tabs on
+ # which Vimium cannot run).
+ checkLastRuntimeError = -> chrome.runtime.lastError
+ chrome.tabs.query { status: "complete" }, (tabs) ->
+ for tab in tabs
+ for [ func, files ] in jobs
+ for file in files
+ func tab.id, { file: file, allFrames: contentScripts.allFrames }, checkLastRuntimeError
+currentVersion = Utils.getCurrentVersion()
tabQueue = {} # windowId -> Array
tabInfoMap = {} # tabId -> object with various tab properties
keyQueue = "" # Queue of keys typed
@@ -9,6 +26,7 @@ validFirstKeys = {}
singleKeyCommands = []
focusedFrame = null
frameIdsForTab = {}
+root.urlForTab = {}
# Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
# This regular expression captures two groups: the first is a named key, the second is the remainder of
@@ -40,7 +58,7 @@ completers =
bookmarks: new MultiCompleter([completionSources.bookmarks])
tabs: new MultiCompleter([completionSources.tabs])
-chrome.runtime.onConnect.addListener((port, name) ->
+chrome.runtime.onConnect.addListener (port, name) ->
senderTabId = if port.sender.tab then port.sender.tab.id else null
# If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore
# the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring
@@ -52,14 +70,8 @@ chrome.runtime.onConnect.addListener((port, name) ->
delete tabLoadedHandlers[senderTabId]
toCall.call()
- # domReady is the appropriate time to show the "vimium has been upgraded" message.
- # TODO: This might be broken on pages with frames.
- if (shouldShowUpgradeMessage())
- chrome.tabs.sendMessage(senderTabId, { name: "showUpgradeNotification", version: currentVersion })
-
if (portHandlers[port.name])
port.onMessage.addListener(portHandlers[port.name])
-)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) ->
if (sendRequestHandlers[request.handler])
@@ -150,22 +162,22 @@ openUrlInCurrentTab = (request) ->
#
# Opens request.url in new tab and switches to it if request.selected is true.
#
-openUrlInNewTab = (request) ->
- chrome.tabs.getSelected(null, (tab) ->
- chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }))
+openUrlInNewTab = (request, callback) ->
+ chrome.tabs.getSelected null, (tab) ->
+ tabConfig =
+ url: Utils.convertToUrl request.url
+ index: tab.index + 1
+ selected: true
+ windowId: tab.windowId
+ # FIXME(smblott). openUrlInNewTab is being called in two different ways with different arguments. We
+ # should refactor it such that this check on callback isn't necessary.
+ callback = (->) unless typeof callback == "function"
+ chrome.tabs.create tabConfig, callback
openUrlInIncognito = (request) ->
chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true})
#
-# Called when the user has clicked the close icon on the "Vimium has been updated" message.
-# We should now dismiss that message in all tabs.
-#
-upgradeNotificationClosed = (request) ->
- Settings.set("previousVersion", currentVersion)
- sendRequestToAllTabs({ name: "hideUpgradeNotification" })
-
-#
# Copies or pastes some data (request.data) to/from the clipboard.
# We return null to avoid the return value from the copy operations being passed to sendResponse.
#
@@ -220,7 +232,14 @@ moveTab = (callback, direction) ->
# These are commands which are bound to keystroke which must be handled by the background page. They are
# mapped in commands.coffee.
BackgroundCommands =
- createTab: (callback) -> chrome.tabs.create({url: Settings.get("newTabUrl")}, (tab) -> callback())
+ createTab: (callback) ->
+ chrome.tabs.query { active: true, currentWindow: true }, (tabs) ->
+ tab = tabs[0]
+ url = Settings.get "newTabUrl"
+ if url == "pages/blank.html"
+ # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
+ url = if tab.incognito then "chrome://newtab" else chrome.runtime.getURL url
+ openUrlInNewTab { url }, callback
duplicateTab: (callback) ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.duplicate(tab.id)
@@ -336,9 +355,6 @@ updateOpenTabs = (tab, deleteFrames = false) ->
# Frames are recreated on refresh
delete frameIdsForTab[tab.id] if deleteFrames
-setBrowserActionIcon = (tabId,path) ->
- chrome.browserAction.setIcon({ tabId: tabId, path: path })
-
chrome.browserAction.setBadgeBackgroundColor
# This is Vimium blue (from the icon).
# color: [102, 176, 226, 255]
@@ -349,43 +365,33 @@ chrome.browserAction.setBadgeBackgroundColor
setBadge = do ->
current = null
timer = null
- updateBadge = (badge) -> -> chrome.browserAction.setBadgeText text: badge
- (request) ->
+ updateBadge = (badge, tabId) -> -> chrome.browserAction.setBadgeText text: badge, tabId: tabId
+ (request, sender) ->
badge = request.badge
if badge? and badge != current
current = badge
clearTimeout timer if timer
# We wait a few moments. This avoids badge flicker when there are rapid changes.
- timer = setTimeout updateBadge(badge), 50
-
-# Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page.
-# Also propagates new enabled/disabled/passkeys state to active window, if necessary.
-# This lets you disable Vimium on a page without needing to reload.
-# Exported via root because it's called from the page popup.
-root.updateActiveState = updateActiveState = (tabId) ->
- enabledIcon = "icons/browser_action_enabled.png"
- disabledIcon = "icons/browser_action_disabled.png"
- partialIcon = "icons/browser_action_partial.png"
- chrome.tabs.get tabId, (tab) ->
- setBadge badge: ""
- chrome.tabs.sendMessage tabId, { name: "getActiveState" }, (response) ->
- if response
- isCurrentlyEnabled = response.enabled
- currentPasskeys = response.passKeys
- config = isEnabledForUrl { url: tab.url }, { tab: tab }
- enabled = config.isEnabledForUrl
- passKeys = config.passKeys
- if (enabled and passKeys)
- setBrowserActionIcon(tabId,partialIcon)
- else if (enabled)
- setBrowserActionIcon(tabId,enabledIcon)
- else
- setBrowserActionIcon(tabId,disabledIcon)
- # Propagate the new state only if it has changed.
- if (isCurrentlyEnabled != enabled || currentPasskeys != passKeys)
- chrome.tabs.sendMessage(tabId, { name: "setState", enabled: enabled, passKeys: passKeys, incognito: tab.incognito })
- else
- setBrowserActionIcon tabId, disabledIcon
+ timer = setTimeout updateBadge(badge, sender.tab.id), 50
+
+# Here's how we set the page icon. The default is "disabled", so if we do nothing else, then we get the
+# grey-out disabled icon. Thereafter, we only set tab-specific icons, so there's no need to update the icon
+# when we visit a tab on which Vimium isn't running.
+#
+# For active tabs, when a frame starts, it requests its active state via isEnabledForUrl. We also check the
+# state every time a frame gets the focus. Once the frame learns its active state, it updates the current
+# tab's badge (but only if that frame has the focus).
+#
+# Exclusion rule changes (from either the options page or the page popup) propagate via the subsequent focus
+# change. In particular, whenever a frame next gets the focus, it requests its new state and sets the icon
+# accordingly.
+#
+setIcon = (request, sender) ->
+ path = switch request.icon
+ when "enabled" then "icons/browser_action_enabled.png"
+ when "partial" then "icons/browser_action_partial.png"
+ when "disabled" then "icons/browser_action_disabled.png"
+ chrome.browserAction.setIcon tabId: sender.tab.id, path: path
handleUpdateScrollPosition = (request, sender) ->
updateScrollPosition(sender.tab, request.scrollX, request.scrollY)
@@ -402,7 +408,6 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
runAt: "document_start"
chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError
updateOpenTabs(tab) if changeInfo.url?
- updateActiveState(tabId)
chrome.tabs.onAttached.addListener (tabId, attachedInfo) ->
# We should update all the tabs in the old window and the new window.
@@ -437,8 +442,7 @@ chrome.tabs.onRemoved.addListener (tabId) ->
tabInfoMap.deletor = -> delete tabInfoMap[tabId]
setTimeout tabInfoMap.deletor, 1000
delete frameIdsForTab[tabId]
-
-chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId)
+ delete urlForTab[tabId]
unless chrome.sessions
chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId]
@@ -595,16 +599,6 @@ sendRequestToAllTabs = (args) ->
for tab in window.tabs
chrome.tabs.sendMessage(tab.id, args, null))
-#
-# Returns true if the current extension version is greater than the previously recorded version in
-# localStorage, and false otherwise.
-#
-shouldShowUpgradeMessage = ->
- # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
- # installs.
- Settings.set("previousVersion", currentVersion) unless Settings.get("previousVersion")
- Utils.compareVersions(currentVersion, Settings.get("previousVersion")) == 1
-
openOptionsPageInNewTab = ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 }))
@@ -622,6 +616,7 @@ unregisterFrame = (request, sender) ->
handleFrameFocused = (request, sender) ->
tabId = sender.tab.id
+ urlForTab[tabId] = request.url
if frameIdsForTab[tabId]?
frameIdsForTab[tabId] =
[request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...]
@@ -643,7 +638,6 @@ sendRequestHandlers =
unregisterFrame: unregisterFrame
frameFocused: handleFrameFocused
nextFrame: (request) -> BackgroundCommands.nextFrame 1, request.frameId
- upgradeNotificationClosed: upgradeNotificationClosed
updateScrollPosition: handleUpdateScrollPosition
copyToClipboard: copyToClipboard
pasteFromClipboard: pasteFromClipboard
@@ -652,6 +646,7 @@ sendRequestHandlers =
refreshCompleter: refreshCompleter
createMark: Marks.create.bind(Marks)
gotoMark: Marks.goto.bind(Marks)
+ setIcon: setIcon
setBadge: setBadge
# We always remove chrome.storage.local/findModeRawQueryListIncognito on startup.
@@ -681,8 +676,30 @@ if Settings.has("keyMappings")
populateValidFirstKeys()
populateSingleKeyCommands()
-if shouldShowUpgradeMessage()
- sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion })
+
+# Show notification on upgrade.
+showUpgradeMessage = ->
+ # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
+ # installs.
+ Settings.set "previousVersion", currentVersion unless Settings.get "previousVersion"
+ if Utils.compareVersions(currentVersion, Settings.get "previousVersion" ) == 1
+ notificationId = "VimiumUpgradeNotification"
+ notification =
+ type: "basic"
+ iconUrl: chrome.runtime.getURL "icons/vimium.png"
+ title: "Vimium Upgrade"
+ message: "Vimium has been upgraded to version #{currentVersion}. Click here for more information."
+ isClickable: true
+ if chrome.notifications?.create?
+ chrome.notifications.create notificationId, notification, ->
+ unless chrome.runtime.lastError
+ Settings.set "previousVersion", currentVersion
+ chrome.notifications.onClicked.addListener (id) ->
+ if id == notificationId
+ openUrlInNewTab url: "https://github.com/philc/vimium#release-notes"
+ else
+ # We need to wait for the user to accept the "notifications" permission.
+ chrome.permissions.onAdded.addListener showUpgradeMessage
# Ensure that tabInfoMap is populated when Vimium is installed.
chrome.windows.getAll { populate: true }, (windows) ->
@@ -695,3 +712,4 @@ chrome.windows.getAll { populate: true }, (windows) ->
# Start pulling changes from synchronized storage.
Sync.init()
+showUpgradeMessage()
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 3528e8a9..a4d95c81 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -33,7 +33,7 @@ root.Settings = Settings =
root.refreshCompletionKeysAfterMappingSave()
searchEngines: (value) ->
- root.Settings.parseSearchEngines value
+ root.SearchEngineCompleter.parseSearchEngines value
exclusionRules: (value) ->
root.Exclusions.postUpdateHook value
@@ -42,27 +42,6 @@ root.Settings = Settings =
performPostUpdateHook: (key, value) ->
@postUpdateHooks[key] value if @postUpdateHooks[key]
- # Here we have our functions that parse the search engines
- # this is a map that we use to store our search engines for use.
- searchEnginesMap: {}
-
- # Parse the custom search engines setting and cache it.
- parseSearchEngines: (searchEnginesText) ->
- @searchEnginesMap = {}
- for line in searchEnginesText.split /\n/
- tokens = line.trim().split /\s+/
- continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#")
- keywords = tokens[0].split ":"
- continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ].
- @searchEnginesMap[keywords[0]] =
- url: tokens[1]
- description: tokens[2..].join(" ")
-
- # Fetch the search-engine map, building it if necessary.
- getSearchEngines: ->
- this.parseSearchEngines(@get("searchEngines") || "") if Object.keys(@searchEnginesMap).length == 0
- @searchEnginesMap
-
# options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
# or strings
defaults:
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 2abfa001..72fde9e1 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -68,6 +68,7 @@ LinkHints =
@hintMode = new Mode
name: "hint/#{mode.name}"
badge: "#{mode.key}?"
+ passInitialKeyupEvents: true
keydown: @onKeyDownInMode.bind(this, hintMarkers),
# trap all key events
keypress: -> false
@@ -231,6 +232,8 @@ LinkHints =
# Remove rects from elements where another clickable element lies above it.
nonOverlappingElements = []
# Traverse the DOM from first to last, since later elements show above earlier elements.
+ # NOTE(smblott). filterHints.generateLinkText also assumes this order when generating the content text for
+ # each hint. Specifically, we consider descendents before we consider their ancestors.
visibleElements = visibleElements.reverse()
while visibleElement = visibleElements.pop()
rects = [visibleElement.rect]
@@ -469,7 +472,7 @@ filterHints =
linkText = element.firstElementChild.alt || element.firstElementChild.title
showLinkText = true if (linkText)
else
- linkText = element.textContent || element.innerHTML
+ linkText = DomUtils.textContent.get element
{ text: linkText, show: showLinkText }
@@ -479,6 +482,7 @@ filterHints =
fillInMarkers: (hintMarkers) ->
@generateLabelMap()
+ DomUtils.textContent.reset()
for marker, idx in hintMarkers
marker.hintString = @generateHintString(idx)
linkTextObject = @generateLinkText(marker.clickableItem)
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index fa583a1c..bded402c 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -120,6 +120,16 @@ class Mode
@registerStateChange?()
registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue
+ # If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page
+ # or to other extensions (because the corresponding keydown events were passed). This is used when
+ # activating link hints, see #1522.
+ if @options.passInitialKeyupEvents
+ @push
+ _name: "mode-#{@id}/passInitialKeyupEvents"
+ keydown: => @alwaysContinueBubbling -> handlerStack.remove()
+ keyup: (event) =>
+ if KeyboardUtils.isPrintable event then @stopBubblingAndFalse else @stopBubblingAndTrue
+
Mode.modes.push @
Mode.updateBadge()
@logModes()
diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee
index 26076123..0b4feb2b 100644
--- a/content_scripts/mode_visual_edit.coffee
+++ b/content_scripts/mode_visual_edit.coffee
@@ -366,12 +366,12 @@ class Movement extends CountPrefix
#
# End of Movement constructor.
- # Yank the selection; always exits; either deletes the selection or removes it; set @yankedText and return
+ # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and return
# it.
yank: (args = {}) ->
@yankedText = @selection.toString()
@selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument
- @selection.removeAllRanges() unless @options.parentMode
+ @selection.collapseToStart() unless @options.parentMode
message = @yankedText.replace /\s+/g, " "
message = message[...12] + "..." if 15 < @yankedText.length
@@ -384,7 +384,7 @@ class Movement extends CountPrefix
exit: (event, target) ->
unless @options.parentMode or @options.oneMovementOnly
- @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event
+ @selection.collapseToStart() if event?.type == "keydown" and KeyboardUtils.isEscape event
# Disabled, pending discussion of fine-tuning the UX. Simpler alternative is implemented above.
# # If we're exiting on escape and there is a range selection, then we leave it in place. However, an
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 5cc3fd82..b7de5140 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -123,6 +123,9 @@ CoreScroller =
@lastEvent = null
@keyIsDown = false
+ # NOTE(smblott) With extreme keyboard configurations, Chrome sometimes does not get a keyup event for
+ # every keydown, in which case tapping "j" scrolls indefinitely. This appears to be a Chrome/OS/XOrg bug
+ # of some kind. See #1549.
handlerStack.push
_name: 'scroller/track-key-status'
keydown: (event) =>
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index c4ed3bf6..dadc84b5 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -29,24 +29,25 @@ class UIComponent
activate: (message) ->
@postMessage message if message?
- if @showing
- # NOTE(smblott) Experimental. Not sure this is a great idea. If the iframe was already showing, then
- # the user gets no visual feedback when it is re-focused. So flash its border.
- @iframeElement.classList.add "vimiumUIComponentReactivated"
- setTimeout((=> @iframeElement.classList.remove "vimiumUIComponentReactivated"), 200)
- else
- @show()
+ @show() unless @showing
@iframeElement.focus()
show: (message) ->
@postMessage message if message?
@iframeElement.classList.remove "vimiumUIComponentHidden"
@iframeElement.classList.add "vimiumUIComponentShowing"
+ window.addEventListener "focus", @onFocus = (event) =>
+ if event.target == window
+ window.removeEventListener "focus", @onFocus
+ @onFocus = null
+ @postMessage "hide"
@showing = true
hide: (focusWindow = true)->
@iframeElement.classList.remove "vimiumUIComponentShowing"
@iframeElement.classList.add "vimiumUIComponentHidden"
+ window.removeEventListener "focus", @onFocus if @onFocus
+ @onFocus = null
window.focus() if focusWindow
@showing = false
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index f549e06d..409a9373 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -26,7 +26,7 @@ validFirstKeys = ""
# The corresponding XPath for such elements.
textInputXPath = (->
- textInputTypes = ["text", "search", "email", "url", "number", "password"]
+ textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
inputElements = ["input[" +
"(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
" and not(@disabled or @readonly)]",
@@ -133,8 +133,6 @@ window.initializeModes = ->
keypress: (event) => onKeypress.call @, event
keyup: (event) => onKeyup.call @, event
- Scroller.init settings
-
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
new BadgeMode
@@ -142,6 +140,7 @@ window.initializeModes = ->
new PassKeysMode
new InsertMode permanent: true
new GrabBackFocus
+ Scroller.init settings
#
# Complete initialization work that sould be done prior to DOMReady.
@@ -163,10 +162,9 @@ initializePreDomReady = ->
isEnabledForUrl = false
chrome.runtime.sendMessage = ->
chrome.runtime.connect = ->
+ window.removeEventListener "focus", onFocus
requestHandlers =
- hideUpgradeNotification: -> HUD.hideUpgradeNotification()
- showUpgradeNotification: (request) -> HUD.showUpgradeNotification(request.version)
showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration
toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId)
focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame(request.highlight)
@@ -174,8 +172,6 @@ initializePreDomReady = ->
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY
executePageCommand: executePageCommand
- getActiveState: getActiveState
- setState: setState
currentKeyQueue: (request) ->
keyQueue = request.keyQueue
handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue }
@@ -184,9 +180,9 @@ initializePreDomReady = ->
# In the options page, we will receive requests from both content and background scripts. ignore those
# from the former.
return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://'
- return unless isEnabledForUrl or request.name == 'getActiveState' or request.name == 'setState'
+ return unless isEnabledForUrl
# These requests are delivered to the options page, but there are no handlers there.
- return if request.handler == "registerFrame" or request.handler == "frameFocused"
+ return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ]
sendResponse requestHandlers[request.name](request, sender)
# Ensure the sendResponse callback is freed.
false
@@ -213,26 +209,22 @@ window.initializeWhenEnabled = ->
installedListeners = true
FindModeHistory.init()
-setState = (request) ->
- isEnabledForUrl = request.enabled
- passKeys = request.passKeys
- isIncognitoMode = request.incognito
- initializeWhenEnabled() if isEnabledForUrl
- handlerStack.bubbleEvent "registerStateChange",
- enabled: isEnabledForUrl
- passKeys: passKeys
-
-getActiveState = ->
- Mode.updateBadge()
- return { enabled: isEnabledForUrl, passKeys: passKeys }
-
#
-# The backend needs to know which frame has focus.
+# Whenever we get the focus:
+# - Reload settings (they may have changed).
+# - Tell the background page this frame's URL.
+# - Check if we should be enabled.
#
-window.addEventListener "focus", ->
- # settings may have changed since the frame last had focus
- settings.load()
- chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId })
+onFocus = (event) ->
+ if event.target == window
+ settings.load()
+ chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId, url: window.location.toString()
+ checkIfEnabledForUrl()
+
+# We install these listeners directly (that is, we don't use installListener) because we still need to receive
+# events when Vimium is not enabled.
+window.addEventListener "focus", onFocus
+window.addEventListener "hashchange", onFocus
#
# Initialization tasks that must wait for the document to be ready.
@@ -565,12 +557,21 @@ checkIfEnabledForUrl = ->
isIncognitoMode = response.incognito
if isEnabledForUrl
initializeWhenEnabled()
- else if (HUD.isReady())
+ else if HUD.isReady()
# Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
HUD.hide()
handlerStack.bubbleEvent "registerStateChange",
enabled: isEnabledForUrl
passKeys: passKeys
+ # Update the page icon, if necessary.
+ if document.hasFocus()
+ chrome.runtime.sendMessage
+ handler: "setIcon"
+ icon:
+ if isEnabledForUrl and not passKeys then "enabled"
+ else if isEnabledForUrl then "partial"
+ else "disabled"
+
# Exported to window, but only for DOM tests.
window.refreshCompletionKeys = (response) ->
@@ -801,8 +802,7 @@ executeFind = (query, options) ->
# previous find landed in an editable element, then that element may still be activated. In this case, we
# don't want to leave it behind (see #1412).
if document.activeElement and DomUtils.isEditable document.activeElement
- if not DomUtils.isSelected document.activeElement
- document.activeElement.blur()
+ document.activeElement.blur() unless DomUtils.isSelected document.activeElement
# we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do
# preventDefault()
@@ -1072,7 +1072,6 @@ toggleHelpDialog = (html, fid) ->
HUD =
_tweenId: -1
_displayElement: null
- _upgradeNotificationElement: null
# This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
# test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
@@ -1090,26 +1089,6 @@ HUD =
HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150)
HUD.displayElement().style.display = ""
- showUpgradeNotification: (version) ->
- HUD.upgradeNotificationElement().innerHTML = "Vimium has been upgraded to #{version}. See
- <a class='vimiumReset' target='_blank'
- href='https://github.com/philc/vimium#release-notes'>
- what's new</a>.<a class='vimiumReset close-button' href='#'>&times;</a>"
- links = HUD.upgradeNotificationElement().getElementsByTagName("a")
- links[0].addEventListener("click", HUD.onUpdateLinkClicked, false)
- links[1].addEventListener "click", (event) ->
- event.preventDefault()
- HUD.onUpdateLinkClicked()
- Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150)
-
- onUpdateLinkClicked: (event) ->
- HUD.hideUpgradeNotification()
- chrome.runtime.sendMessage({ handler: "upgradeNotificationClosed" })
-
- hideUpgradeNotification: (clickEvent) ->
- Tween.fade(HUD.upgradeNotificationElement(), 0, 150,
- -> HUD.upgradeNotificationElement().style.display = "none")
-
#
# Retrieves the HUD HTML element.
#
@@ -1120,13 +1099,6 @@ HUD =
HUD._displayElement.style.right = "150px"
HUD._displayElement
- upgradeNotificationElement: ->
- if (!HUD._upgradeNotificationElement)
- HUD._upgradeNotificationElement = HUD.createHudElement()
- # Position this just to the left of our normal HUD.
- HUD._upgradeNotificationElement.style.right = "315px"
- HUD._upgradeNotificationElement
-
createHudElement: ->
element = document.createElement("div")
element.className = "vimiumReset vimiumHUD"
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 6381fd7f..c4cfc8b9 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -38,7 +38,10 @@ Vomnibar =
init: ->
unless @vomnibarUI?
@vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", (event) =>
- @vomnibarUI.hide() if event.data == "hide"
+ if event.data == "hide"
+ @vomnibarUI.hide()
+ @vomnibarUI.postMessage "hidden"
+
# This function opens the vomnibar. It accepts options, a map with the values:
# completer - The completer to fetch results from.
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 2ae9412e..0231f994 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -167,15 +167,20 @@ DomUtils =
node = node.parentNode
false
- # True if element contains the active selection range.
+ # True if element is editable and contains the active selection range.
isSelected: (element) ->
+ selection = document.getSelection()
if element.isContentEditable
- node = document.getSelection()?.anchorNode
+ node = selection.anchorNode
node and @isDOMDescendant element, node
else
- # Note. This makes the wrong decision if the user has placed the caret at the start of element. We
- # cannot distinguish that case from the user having made no selection.
- element.selectionStart? and element.selectionEnd? and element.selectionEnd != 0
+ if selection.type == "Range" and selection.isCollapsed
+ # The selection is inside the Shadow DOM of a node. We can check the node it registers as being
+ # before, since this represents the node whose Shadow DOM it's inside.
+ containerNode = selection.anchorNode.childNodes[selection.anchorOffset]
+ element == containerNode # True if the selection is inside the Shadow DOM of our element.
+ else
+ false
simulateSelect: (element) ->
# If element is already active, then we don't move the selection. However, we also won't get a new focus
@@ -185,11 +190,17 @@ DomUtils =
handlerStack.bubbleEvent "click", target: element
else
element.focus()
- unless @isSelected element
- # When focusing a textbox (without an existing selection), put the selection caret at the end of the
- # textbox's contents. For some HTML5 input types (eg. date) we can't position the caret, so we wrap
- # this with a try.
- try element.setSelectionRange(element.value.length, element.value.length)
+ # If the cursor is at the start of the element's contents, send it to the end. Motivation:
+ # * the end is a more useful place to focus than the start,
+ # * this way preserves the last used position (except when it's at the beginning), so the user can
+ # 'resume where they left off'.
+ # NOTE(mrmr1993): Some elements throw an error when we try to access their selection properties, so
+ # wrap this with a try.
+ try
+ if element.selectionStart == 0 and element.selectionEnd == 0
+ element.setSelectionRange element.value.length, element.value.length
+
+
simulateClick: (element, modifiers) ->
modifiers ||= {}
@@ -295,5 +306,24 @@ DomUtils =
document.body.removeChild div
coordinates
+ # Get the text content of an element (and its descendents), but omit the text content of previously-visited
+ # nodes. See #1514.
+ # NOTE(smblott). This is currently O(N^2) (when called on N elements). An alternative would be to mark
+ # each node visited, and then clear the marks when we're done.
+ textContent: do ->
+ visitedNodes = null
+ reset: -> visitedNodes = []
+ get: (element) ->
+ nodes = document.createTreeWalker element, NodeFilter.SHOW_TEXT
+ texts =
+ while node = nodes.nextNode()
+ continue unless node.nodeType == 3
+ continue if node in visitedNodes
+ text = node.data.trim()
+ continue unless 0 < text.length
+ visitedNodes.push node
+ text
+ texts.join " "
+
root = exports ? window
root.DomUtils = DomUtils
diff --git a/manifest.json b/manifest.json
index beb68530..4ba222fb 100644
--- a/manifest.json
+++ b/manifest.json
@@ -27,6 +27,7 @@
"clipboardRead",
"storage",
"sessions",
+ "notifications",
"<all_urls>"
],
"content_scripts": [
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 0884f2cd..77c3e2bf 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -46,6 +46,7 @@
</div>
<div class="vimiumReset vimiumColumn" style="text-align:right">
<span class="vimiumReset">Version {{version}}</span><br/>
+ <a href="https://github.com/philc/vimium#release-notes" class="vimiumReset">What's new?</a>
</div>
</div>
</div>
diff --git a/pages/options.coffee b/pages/options.coffee
index d2950348..6545189b 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -271,8 +271,12 @@ initPopupPage = ->
exclusions = null
document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
+ # As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own
+ # URL.
+ url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url
+
updateState = ->
- rule = bgExclusions.getRule tab.url, exclusions.readValueFromElement()
+ rule = bgExclusions.getRule url, exclusions.readValueFromElement()
$("state").innerHTML = "Vimium will " +
if rule and rule.passKeys
"exclude <span class='code'>#{rule.passKeys}</span>"
@@ -291,8 +295,6 @@ initPopupPage = ->
Option.saveOptions()
$("saveOptions").innerHTML = "Saved"
$("saveOptions").disabled = true
- chrome.tabs.query { windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, (tabs) ->
- chrome.extension.getBackgroundPage().updateActiveState(tabs[0].id)
$("saveOptions").addEventListener "click", saveOptions
@@ -302,7 +304,7 @@ initPopupPage = ->
window.close()
# Populate options. Just one, here.
- exclusions = new ExclusionRulesOnPopupOption(tab.url, "exclusionRules", onUpdated)
+ exclusions = new ExclusionRulesOnPopupOption url, "exclusionRules", onUpdated
updateState()
document.addEventListener "keyup", updateState
diff --git a/pages/options.html b/pages/options.html
index 889d5ea0..f89ddcbb 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -200,7 +200,7 @@ b: http://b.com/?q=%s description
<div class="help">
<div class="example">
The page to open with the "create new tab" command.
- Set this to "<tt>pages/blank.html</tt>" for a blank page.<br />
+ Set this to "<tt>pages/blank.html</tt>" for a blank page (except incognito mode).<br />
</div>
</div>
<input id="newTabUrl" type="text" />
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 18a72a37..06ec9ee9 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -38,9 +38,13 @@ Vomnibar =
@vomnibarUI.setQuery(options.query)
@vomnibarUI.update()
+ hide: -> @vomnibarUI?.hide()
+ onHidden: -> @vomnibarUI?.onHidden()
+
class VomnibarUI
constructor: ->
@refreshInterval = 0
+ @postHideCallback = null
@initDom()
setQuery: (query) -> @input.value = query
@@ -57,9 +61,20 @@ class VomnibarUI
setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab
- hide: ->
+ # The sequence of events when the vomnibar is hidden is as follows:
+ # 1. Post a "hide" message to the host page.
+ # 2. The host page hides the vomnibar and posts back a "hidden" message.
+ # 3. Only once "hidden" message is received here is any required action (callback) invoked (in onHidden).
+ # This ensures that the vomnibar is actually hidden, and avoids flicker after opening a link in a new tab
+ # (see #1485).
+ hide: (callback = null) ->
UIComponentServer.postMessage "hide"
@reset()
+ @postHideCallback = callback
+
+ onHidden: ->
+ @postHideCallback?()
+ @postHideCallback = null
reset: ->
@completionList.style.display = ""
@@ -121,15 +136,15 @@ class VomnibarUI
query = @input.value.trim()
# <Enter> on an empty vomnibar is a no-op.
return unless 0 < query.length
- @hide()
- chrome.runtime.sendMessage({
- handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
- url: query })
+ @hide ->
+ chrome.runtime.sendMessage
+ handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
+ url: query
else
@update true, =>
# Shift+Enter will open the result in a new tab instead of the current tab.
- @completions[@selection].performAction(openInNewTab)
- @hide()
+ completion = @completions[@selection]
+ @hide -> completion.performAction openInNewTab
# It seems like we have to manually suppress the event here and still return true.
event.stopImmediatePropagation()
@@ -180,6 +195,12 @@ class VomnibarUI
@completionList.style.display = ""
window.addEventListener "focus", => @input.focus()
+ # A click in the vomnibar itself refocuses the input.
+ @box.addEventListener "click", (event) =>
+ @input.focus()
+ event.stopImmediatePropagation()
+ # A click anywhere else hides the vomnibar.
+ document.body.addEventListener "click", => @hide()
#
# Sends filter and refresh requests to a Vomnibox completer on the background page.
@@ -225,7 +246,11 @@ extend BackgroundCompleter,
switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId })
-UIComponentServer.registerHandler (event) -> Vomnibar.activate event.data
+UIComponentServer.registerHandler (event) ->
+ switch event.data
+ when "hide" then Vomnibar.hide()
+ when "hidden" then Vomnibar.onHidden()
+ else Vomnibar.activate event.data
root = exports ? window
root.Vomnibar = Vomnibar
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index a1ca8723..4afa9d7d 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -84,6 +84,47 @@ createGeneralHintTests = (isFilteredMode) ->
createGeneralHintTests false
createGeneralHintTests true
+inputs = []
+context "Test link hints for focusing input elements correctly",
+
+ setup ->
+ initializeModeState()
+ testDiv = document.getElementById("test-div")
+ testDiv.innerHTML = ""
+
+ stub settings.values, "filterLinkHints", false
+ stub settings.values, "linkHintCharacters", "ab"
+
+ # Every HTML5 input type except for hidden. We should be able to activate all of them with link hints.
+ inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file",
+ "image", "month", "number", "password", "radio", "range", "reset", "search", "submit", "tel", "text",
+ "time", "url", "week"]
+
+ for type in inputTypes
+ input = document.createElement "input"
+ input.type = type
+ testDiv.appendChild input
+ inputs.push input
+
+ tearDown ->
+ document.getElementById("test-div").innerHTML = ""
+
+ should "Focus each input when its hint text is typed", ->
+ for input in inputs
+ input.scrollIntoView() # Ensure the element is visible so we create a link hint for it.
+
+ activeListener = ensureCalled (event) ->
+ input.blur() if event.type == "focus"
+ input.addEventListener "focus", activeListener, false
+ input.addEventListener "click", activeListener, false
+
+ LinkHints.activateMode()
+ [hint] = getHintMarkers().filter (hint) -> input == hint.clickableItem
+ sendKeyboardEvent char for char in hint.hintString
+
+ input.removeEventListener "focus", activeListener, false
+ input.removeEventListener "click", activeListener, false
+
context "Alphabetical link hints",
setup ->
@@ -501,7 +542,8 @@ context "Mode badges",
should "have an I badge in insert mode by focus", ->
document.getElementById("first").focus()
- assert.isTrue chromeMessages[0].badge == "I"
+ # Focus triggers an event in the handler stack, so we check element "1", here.
+ assert.isTrue chromeMessages[1].badge == "I"
should "have no badge after leaving insert mode by focus", ->
document.getElementById("first").focus()
@@ -534,5 +576,6 @@ context "Mode badges",
passKeys: ""
document.getElementById("first").focus()
- assert.isTrue chromeMessages[0].badge == ""
+ # Focus triggers an event in the handler stack, so we check element "1", here.
+ assert.isTrue chromeMessages[1].badge == ""
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index b7b73cc2..56fcc456 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -152,8 +152,9 @@ context "domain completer",
setup ->
@history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(1) }
@history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) }
+ @undef = { title: "history2", url: "http://undefined.net", lastVisitTime: hours(1) }
- stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2]))
+ stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2, @undef]))
global.chrome.history =
onVisited: { addListener: -> }
onVisitRemoved: { addListener: -> }
@@ -174,6 +175,9 @@ context "domain completer",
should "returns no results when there's more than one query term, because clearly it's not a domain", ->
assert.arrayEqual [], filterCompleter(@completer, ["his", "tory"])
+ should "not return any results for empty queries", ->
+ assert.arrayEqual [], filterCompleter(@completer, [])
+
context "domain completer (removing entries)",
setup ->
@history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(2) }
@@ -238,7 +242,7 @@ context "search engines",
@completer = new SearchEngineCompleter()
# note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors
# workaround is below, would be good for someone that understands the testing system better than me to improve
- @completer.searchEngines = Settings.getSearchEngines()
+ @completer.searchEngines = SearchEngineCompleter.getSearchEngines()
should "return search engine suggestion without description", ->
results = filterCompleter(@completer, ["foo", "hello"])
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index afe862a4..346c98da 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -73,7 +73,7 @@ context "settings",
should "set search engines, retrieve them correctly and check that they have been parsed correctly", ->
searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description"
Settings.set 'searchEngines', searchEngines
- result = Settings.getSearchEngines()
+ result = SearchEngineCompleter.getSearchEngines()
assert.equal Object.keys(result).length, 2
assert.equal "bar?q=%s", result["foo"].url
assert.isFalse result["foo"].description
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index c61d7246..bc50521a 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -16,6 +16,8 @@ exports.chrome =
addListener: () -> true
onMessage:
addListener: () -> true
+ onInstalled:
+ addListener: ->
tabs:
onSelectionChanged: