aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/commands.coffee2
-rw-r--r--background_scripts/completion.coffee3
-rw-r--r--background_scripts/main.coffee5
-rw-r--r--background_scripts/sync.coffee38
-rw-r--r--content_scripts/mode.coffee12
-rw-r--r--content_scripts/mode_insert.coffee2
-rw-r--r--content_scripts/scroller.coffee4
-rw-r--r--content_scripts/vimium_frontend.coffee271
-rw-r--r--lib/handler_stack.coffee5
-rw-r--r--lib/keyboard_utils.coffee4
-rw-r--r--pages/vomnibar.coffee14
-rw-r--r--tests/dom_tests/chrome.coffee5
-rw-r--r--tests/dom_tests/dom_tests.coffee542
-rw-r--r--tests/dom_tests/phantom_runner.coffee12
14 files changed, 330 insertions, 589 deletions
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index ae065f55..79cb9ee0 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -58,7 +58,7 @@ Commands =
for line in lines
continue if (line[0] == "\"" || line[0] == "#")
- splitLine = line.split(/\s+/)
+ splitLine = line.replace(/\s+$/, "").split(/\s+/)
lineCommand = splitLine[0]
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index d6402019..177892fb 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -21,6 +21,8 @@ class Suggestion
# - extraRelevancyData: data (like the History item itself) which may be used by the relevancy function.
constructor: (@queryTerms, @type, @url, @title, @computeRelevancyFunction, @extraRelevancyData) ->
@title ||= ""
+ # When @autoSelect is truthy, the suggestion is automatically pre-selected in the vomnibar.
+ @autoSelect = false
computeRelevancy: -> @relevancy = @computeRelevancyFunction(this)
@@ -335,6 +337,7 @@ class SearchEngineCompleter
type = "search"
query = queryTerms[0] + ": " + queryTerms[1..].join(" ")
suggestion = new Suggestion(queryTerms, type, url, query, @computeRelevancy)
+ suggestion.autoSelect = true
suggestions.push(suggestion)
onComplete(suggestions)
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 37d219df..5a126ceb 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -169,9 +169,10 @@ upgradeNotificationClosed = (request) ->
#
# 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.
#
-copyToClipboard = (request) -> Clipboard.copy(request.data)
-pasteFromClipboard = (request) -> Clipboard.paste()
+copyToClipboard = (request) -> Clipboard.copy(request.data); null
+pasteFromClipboard = (request) -> Clipboard.paste(); null
#
# Selects the tab with the ID specified in request.id
diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee
index 93430856..ad59f958 100644
--- a/background_scripts/sync.coffee
+++ b/background_scripts/sync.coffee
@@ -20,11 +20,6 @@
root = exports ? window
root.Sync = Sync =
- # April 19 2014: Leave logging statements in, but disable debugging. We may need to come back to this, so
- # removing logging now would be premature. However, if users report problems, they are unlikely to notice
- # and make sense of console logs on background pages. So disable it, by default. For genuine errors, we
- # call console.log directly.
- debug: false
storage: chrome.storage.sync
doNotSync: ["settingsVersion", "previousVersion"]
@@ -36,19 +31,13 @@ root.Sync = Sync =
# Asynchronous fetch from synced storage, called only at startup.
fetchAsync: ->
@storage.get null, (items) =>
- # Chrome sets chrome.runtime.lastError if there is an error.
- if chrome.runtime.lastError is undefined
+ unless chrome.runtime.lastError
for own key, value of items
- @log "fetchAsync: #{key} <- #{value}"
@storeAndPropagate key, value
- else
- console.log "callback for Sync.fetchAsync() indicates error"
- console.log chrome.runtime.lastError
# Asynchronous message from synced storage.
handleStorageUpdate: (changes, area) ->
for own key, change of changes
- @log "handleStorageUpdate: #{key} <- #{change.newValue}"
@storeAndPropagate key, change?.newValue
# Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
@@ -61,12 +50,10 @@ root.Sync = Sync =
if value and value != defaultValueJSON
# Key/value has been changed to non-default value at remote instance.
- @log "storeAndPropagate update: #{key}=#{value}"
localStorage[key] = value
Settings.performPostUpdateHook key, JSON.parse(value)
else
# Key has been reset to default value at remote instance.
- @log "storeAndPropagate clear: #{key}"
if key of localStorage
delete localStorage[key]
Settings.performPostUpdateHook key, defaultValue
@@ -75,28 +62,13 @@ root.Sync = Sync =
# No need to propagate updates to the rest of vimium, that's already been done.
set: (key, value) ->
if @shouldSyncKey key
- @log "set scheduled: #{key}=#{value}"
- key_value = {}
- key_value[key] = value
- @storage.set key_value, =>
- # Chrome sets chrome.runtime.lastError if there is an error.
- if chrome.runtime.lastError
- console.log "callback for Sync.set() indicates error: #{key} <- #{value}"
- console.log chrome.runtime.lastError
+ setting = {}; setting[key] = value
+ @storage.set setting
# Only called synchronously from within vimium, never on a callback.
clear: (key) ->
- if @shouldSyncKey key
- @log "clear scheduled: #{key}"
- @storage.remove key, =>
- # Chrome sets chrome.runtime.lastError if there is an error.
- if chrome.runtime.lastError
- console.log "for Sync.clear() indicates error: #{key}"
- console.log chrome.runtime.lastError
+ @storage.remove key if @shouldSyncKey key
# Should we synchronize this key?
- shouldSyncKey: (key) ->
- key not in @doNotSync
+ shouldSyncKey: (key) -> key not in @doNotSync
- log: (msg) ->
- console.log "Sync: #{msg}" if @debug
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 2733de8b..cc358bc2 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -176,14 +176,19 @@ class Mode
log: (args...) ->
console.log args... if @debug
- # Return the must-recently activated mode (only used in tests).
+ # For tests only.
@top: ->
@modes[@modes.length-1]
+ # For tests only.
+ @reset: ->
+ mode.exit() for mode in @modes
+ @modes = []
+
# BadgeMode is a pseudo mode for triggering badge updates on focus changes and state updates. It sits at the
# bottom of the handler stack, and so it receives state changes *after* all other modes, and can override the
-# badge choice of the other modes. We create the the one-and-only instance here.
-new class BadgeMode extends Mode
+# badge choice of the other modes.
+class BadgeMode extends Mode
constructor: () ->
super
name: "badge"
@@ -207,3 +212,4 @@ new class BadgeMode extends Mode
root = exports ? window
root.Mode = Mode
+root.BadgeMode = BadgeMode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index dfa60a3d..90162d5a 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -53,7 +53,7 @@ class InsertMode extends Mode
if @insertModeLock != event.target and DomUtils.isFocusable event.target
@activateOnElement event.target
- # Only for tests. This gives us a hook to test the status of the permanent instance.
+ # Only for tests. This gives us a hook to test the status of the permanently-installed instance.
InsertMode.permanentInstance = @ if @permanent
isActive: (event) ->
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 08cc0779..5cc3fd82 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -228,7 +228,7 @@ Scroller =
window.scrollBy(0, amount)
return
- activatedElement ||= document.body and firstScrollableElement()
+ activatedElement ||= (document.body and firstScrollableElement()) or document.body
return unless activatedElement
# Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth,
@@ -239,7 +239,7 @@ Scroller =
CoreScroller.scroll element, direction, elementAmount
scrollTo: (direction, pos) ->
- activatedElement ||= document.body and firstScrollableElement()
+ activatedElement ||= (document.body and firstScrollableElement()) or document.body
return unless activatedElement
element = findScrollableElement activatedElement, direction, pos, 1
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 6b96e929..4fdf58bd 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -5,18 +5,12 @@
# "domReady".
#
-targetElement = null
-findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
findModeQueryHasResults = false
findModeAnchorNode = null
findModeInitialRange = null
isShowingHelpDialog = false
keyPort = null
-# Users can disable Vimium on URL patterns via the settings page. The following two variables
-# (isEnabledForUrl and passKeys) control Vimium's enabled/disabled behaviour.
-# "passKeys" are keys which would normally be handled by Vimium, but are disabled on this tab, and therefore
-# are passed through to the underlying page.
isEnabledForUrl = true
passKeys = null
keyQueue = null
@@ -48,7 +42,7 @@ settings =
values: {}
loadedValues: 0
valuesToLoad: ["scrollStepSize", "linkHintCharacters", "linkHintNumbers", "filterLinkHints", "hideHud",
- "previousPatterns", "nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss",
+ "previousPatterns", "nextPatterns", "findModeRawQuery", "findModeRawQueryList", "regexFindMode", "userDefinedLinkHintCss",
"helpDialog_showAdvancedCommands", "smoothScroll"]
isLoaded: false
eventListeners: {}
@@ -101,15 +95,8 @@ settings =
#
frameId = Math.floor(Math.random()*999999999)
-hasModifiersRegex = /^<([amc]-)+.>/
-
-#
-# Complete initialization work that sould be done prior to DOMReady.
-#
-initializePreDomReady = ->
- settings.addEventListener("load", LinkHints.init.bind(LinkHints))
- settings.load()
-
+# Only exported for tests.
+window.initializeModes = ->
class NormalMode extends Mode
constructor: ->
super
@@ -122,12 +109,20 @@ initializePreDomReady = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
+ new BadgeMode
new NormalMode
new PassKeysMode
new InsertMode permanent: true
- checkIfEnabledForUrl()
+#
+# Complete initialization work that sould be done prior to DOMReady.
+#
+initializePreDomReady = ->
+ settings.addEventListener("load", LinkHints.init.bind(LinkHints))
+ settings.load()
+ initializeModes()
+ checkIfEnabledForUrl()
refreshCompletionKeys()
# Send the key to the key handler in the background page.
@@ -179,25 +174,22 @@ installListener = (element, event, callback) ->
# Run this as early as possible, so the page can't register any event handlers before us.
#
installedListeners = false
-initializeWhenEnabled = (newPassKeys) ->
- isEnabledForUrl = true
- passKeys = newPassKeys
- if (!installedListeners)
+window.initializeWhenEnabled = ->
+ unless installedListeners
# Key event handlers fire on window before they do on document. Prefer window for key events so the page
# can't set handlers to grab the keys before us.
for type in ["keydown", "keypress", "keyup", "click", "focus", "blur"]
do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
- installListener document, "DOMActivate", onDOMActivate
- enterInsertModeIfElementIsFocused()
+ installListener document, "DOMActivate", (event) -> handlerStack.bubbleEvent 'DOMActivate', event
installedListeners = true
setState = (request) ->
- initializeWhenEnabled(request.passKeys) if request.enabled
isEnabledForUrl = request.enabled
passKeys = request.passKeys
+ initializeWhenEnabled() if isEnabledForUrl
handlerStack.bubbleEvent "registerStateChange",
- enabled: request.enabled
- passKeys: request.passKeys
+ enabled: isEnabledForUrl
+ passKeys: passKeys
getActiveState = ->
Mode.updateBadge()
@@ -215,8 +207,6 @@ window.addEventListener "focus", ->
# Initialization tasks that must wait for the document to be ready.
#
initializeOnDomReady = ->
- enterInsertModeIfElementIsFocused() if isEnabledForUrl
-
# Tell the background page we're in the dom ready state.
chrome.runtime.connect({ name: "domReady" })
CursorHider.init()
@@ -236,15 +226,6 @@ unregisterFrame = ->
frameId: frameId
tab_is_closing: window.top == window.self
-#
-# Enters insert mode if the currently focused element in the DOM is focusable.
-#
-enterInsertModeIfElementIsFocused = ->
- if (document.activeElement && isEditable(document.activeElement) && !findMode)
- enterInsertModeWithoutShowingIndicator(document.activeElement)
-
-onDOMActivate = (event) -> handlerStack.bubbleEvent 'DOMActivate', event
-
executePageCommand = (request) ->
return unless frameId == request.frameId
@@ -340,11 +321,9 @@ extend window,
focusInput: do ->
# Track the most recently focused input element.
recentlyFocusedElement = null
- handlerStack.push
- _name: "focus-input-tracker"
- focus: (event) ->
- recentlyFocusedElement = event.target if DomUtils.isEditable event.target
- true
+ window.addEventListener "focus",
+ (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target
+ , true
(count, mode = InsertMode) ->
# Focus the first input element on the page, and create overlays to highlight all the input elements, with
@@ -359,7 +338,9 @@ extend window,
continue if rect == null
{ element: element, rect: rect }
- return if visibleInputs.length == 0
+ if visibleInputs.length == 0
+ HUD.showForDuration("There are no inputs to focus.", 1000)
+ return
selectedInputIndex =
if count == 1
@@ -423,13 +404,6 @@ extend window,
singleton: document.activeElement
targetElement: document.activeElement
-# Decide whether this keyChar should be passed to the underlying page.
-# Keystrokes are *never* considered passKeys if the keyQueue is not empty. So, for example, if 't' is a
-# passKey, then 'gt' and '99t' will neverthless be handled by vimium.
-isPassKey = ( keyChar ) ->
- return false # Disabled.
- return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar)
-
# Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup
# event.
KeydownEvents =
@@ -469,25 +443,13 @@ onKeypress = (event) ->
if (event.keyCode > 31)
keyChar = String.fromCharCode(event.charCode)
- # Enter insert mode when the user enables the native find interface.
- if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event))
- enterInsertModeWithoutShowingIndicator()
- return @stopBubblingAndTrue
-
if (keyChar)
- if (findMode)
- handleKeyCharForFindMode(keyChar)
+ if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
DomUtils.suppressEvent(event)
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @stopBubblingAndTrue
- else if (!isInsertMode() && !findMode)
- if (isPassKey keyChar)
- return @stopBubblingAndTrue
- if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
- DomUtils.suppressEvent(event)
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
- return @stopBubblingAndTrue
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @continueBubbling
@@ -520,50 +482,13 @@ onKeydown = (event) ->
if (modifiers.length > 0 || keyChar.length > 1)
keyChar = "<" + keyChar + ">"
- if (isInsertMode() && KeyboardUtils.isEscape(event))
- if isEditable(event.srcElement) or isEmbed(event.srcElement)
- # Remove focus so the user can't just get himself back into insert mode by typing in the same input
- # box.
- # NOTE(smblott, 2014/12/22) Including embeds for .blur() etc. here is experimental. It appears to be
- # the right thing to do for most common use cases. However, it could also cripple flash-based sites and
- # games. See discussion in #1211 and #1194.
- event.srcElement.blur()
- exitInsertMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (findMode)
- if (KeyboardUtils.isEscape(event))
- handleEscapeForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
- handleDeleteForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (event.keyCode == keyCodes.enter)
- handleEnterForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (!modifiers)
- DomUtils.suppressPropagation(event)
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
+ if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
DomUtils.suppressEvent event
KeydownEvents.push event
return @stopBubblingAndTrue
- else if (!isInsertMode() && !findMode)
+ else
if (keyChar)
if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent event
@@ -576,9 +501,6 @@ onKeydown = (event) ->
else if (KeyboardUtils.isEscape(event))
keyPort.postMessage({ keyChar:"<ESC>", frameId:frameId })
- else if isPassKey KeyboardUtils.getKeyChar(event)
- return undefined
-
# Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command.
# The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us
# back into the search box. As a side effect, this should also prevent overriding by other sites.
@@ -586,9 +508,9 @@ onKeydown = (event) ->
# Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress).
#
# TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
- if (keyChar == "" && !isInsertMode() &&
+ if keyChar == "" &&
(currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
- isValidFirstKey(KeyboardUtils.getKeyChar(event))))
+ isValidFirstKey(KeyboardUtils.getKeyChar(event)))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
return @stopBubblingAndTrue
@@ -606,14 +528,15 @@ checkIfEnabledForUrl = ->
chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url }, (response) ->
isEnabledForUrl = response.isEnabledForUrl
- if (isEnabledForUrl)
- initializeWhenEnabled(response.passKeys)
+ passKeys = response.passKeys
+ if isEnabledForUrl
+ initializeWhenEnabled()
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: response.isEnabledForUrl
- passKeys: response.passKeys
+ enabled: isEnabledForUrl
+ passKeys: passKeys
# Exported to window, but only for DOM tests.
window.refreshCompletionKeys = (response) ->
@@ -628,57 +551,27 @@ window.refreshCompletionKeys = (response) ->
isValidFirstKey = (keyChar) ->
validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
-onFocusCapturePhase = (event) ->
- if (isFocusable(event.target) && !findMode)
- enterInsertModeWithoutShowingIndicator(event.target)
-
-onBlurCapturePhase = (event) ->
- if (isFocusable(event.target))
- exitInsertMode(event.target)
-
-#
-# Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus.
-#
-isFocusable = (element) -> isEditable(element) || isEmbed(element)
-
-#
-# Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
-# unfocused.
-#
-isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase()) >= 0
-
-#
-# Input or text elements are considered focusable and able to receieve their own keyboard events,
-# and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on
-# any element which makes it a rich text editor, like the notes on jjot.com.
-#
-isEditable = (target) ->
- # Note: document.activeElement.isContentEditable is also rechecked in isInsertMode() dynamically.
- return true if target.isContentEditable
- nodeName = target.nodeName.toLowerCase()
- # use a blacklist instead of a whitelist because new form controls are still being implemented for html5
- noFocus = ["radio", "checkbox"]
- if (nodeName == "input" && noFocus.indexOf(target.type) == -1)
- return true
- focusableElements = ["textarea", "select"]
- focusableElements.indexOf(nodeName) >= 0
-
-#
-# We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A
-# causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode
-# when the last editable element that came into focus -- which targetElement points to -- has been blurred.
-# If insert mode is entered manually (via pressing 'i'), then we set targetElement to 'undefined', and only
-# leave insert mode when the user presses <ESC>.
-# Note. This returns the truthiness of target, which is required by isInsertMode.
-#
-enterInsertModeWithoutShowingIndicator = (target) ->
- return # Disabled.
-
-exitInsertMode = (target) ->
- return # Disabled.
-
-isInsertMode = ->
- return false # Disabled.
+# This implements a find-mode query history (using the "findModeRawQueryList" setting) as a list of raw
+# queries, most recent first.
+FindModeHistory =
+ getQuery: (index = 0) ->
+ @migration()
+ recentQueries = settings.get "findModeRawQueryList"
+ if index < recentQueries.length then recentQueries[index] else ""
+
+ recordQuery: (query) ->
+ @migration()
+ if 0 < query.length
+ recentQueries = settings.get "findModeRawQueryList"
+ settings.set "findModeRawQueryList", ([ query ].concat recentQueries.filter (q) -> q != query)[0..50]
+
+ # Migration (from 1.49, 2015/2/1).
+ # Legacy setting: findModeRawQuery (a string).
+ # New setting: findModeRawQueryList (a list of strings).
+ migration: ->
+ unless settings.get "findModeRawQueryList"
+ rawQuery = settings.get "findModeRawQuery"
+ settings.set "findModeRawQueryList", (if rawQuery then [ rawQuery ] else [])
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
@@ -706,6 +599,9 @@ updateFindModeQuery = ->
# default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(findModeQuery.parsedQuery)
+ # Don't count matches in the HUD.
+ HUD.hide(true)
+
# if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
# sequentially so the browser handles the scrolling / text selection.
if findModeQuery.isRegex
@@ -731,12 +627,15 @@ updateFindModeQuery = ->
text = document.body.innerText
findModeQuery.matchCount = text.match(pattern)?.length
-handleKeyCharForFindMode = (keyChar) ->
- findModeQuery.rawQuery += keyChar
+updateQueryForFindMode = (rawQuery) ->
+ findModeQuery.rawQuery = rawQuery
updateFindModeQuery()
performFindInPlace()
showFindModeHUDForQuery()
+handleKeyCharForFindMode = (keyChar) ->
+ updateQueryForFindMode findModeQuery.rawQuery + keyChar
+
handleEscapeForFindMode = ->
exitFindMode()
document.body.classList.remove("vimiumFindMode")
@@ -749,15 +648,15 @@ handleEscapeForFindMode = ->
window.getSelection().addRange(range)
focusFoundLink() || selectFoundInputElement()
+# Return true if character deleted, false otherwise.
handleDeleteForFindMode = ->
- if (findModeQuery.rawQuery.length == 0)
+ if findModeQuery.rawQuery.length == 0
exitFindMode()
performFindInPlace()
+ false
else
- findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
- updateFindModeQuery()
- performFindInPlace()
- showFindModeHUDForQuery()
+ updateQueryForFindMode findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
+ true
# <esc> sends us into insert mode if possible, but <cr> does not.
# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
@@ -766,10 +665,12 @@ handleEnterForFindMode = ->
exitFindMode()
focusFoundLink()
document.body.classList.add("vimiumFindMode")
- settings.set("findModeRawQuery", findModeQuery.rawQuery)
+ FindModeHistory.recordQuery findModeQuery.rawQuery
class FindMode extends Mode
constructor: ->
+ @historyIndex = -1
+ @partialQuery = ""
super
name: "find"
badge: "/"
@@ -778,12 +679,23 @@ class FindMode extends Mode
keydown: (event) =>
if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
- handleDeleteForFindMode()
+ @exit() unless handleDeleteForFindMode()
@suppressEvent
else if event.keyCode == keyCodes.enter
handleEnterForFindMode()
@exit()
@suppressEvent
+ else if event.keyCode == keyCodes.upArrow
+ if rawQuery = FindModeHistory.getQuery @historyIndex + 1
+ @historyIndex += 1
+ @partialQuery = findModeQuery.rawQuery if @historyIndex == 0
+ updateQueryForFindMode rawQuery
+ @suppressEvent
+ else if event.keyCode == keyCodes.downArrow
+ @historyIndex = Math.max -1, @historyIndex - 1
+ rawQuery = if 0 <= @historyIndex then FindModeHistory.getQuery @historyIndex else @partialQuery
+ updateQueryForFindMode rawQuery
+ @suppressEvent
else
DomUtils.suppressPropagation(event)
handlerStack.stopBubblingAndFalse
@@ -854,8 +766,6 @@ selectFoundInputElement = ->
DomUtils.isSelectable(document.activeElement) &&
DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
DomUtils.simulateSelect(document.activeElement)
- # the element has already received focus via find(), so invoke insert mode manually
- enterInsertModeWithoutShowingIndicator(document.activeElement)
getNextQueryFromRegexMatches = (stepSize) ->
# find()ing an empty query always returns false
@@ -869,7 +779,7 @@ getNextQueryFromRegexMatches = (stepSize) ->
window.getFindModeQuery = ->
# check if the query has been changed by a script in another frame
- mostRecentQuery = settings.get("findModeRawQuery") || ""
+ mostRecentQuery = FindModeHistory.getQuery()
if (mostRecentQuery != findModeQuery.rawQuery)
findModeQuery.rawQuery = mostRecentQuery
updateFindModeQuery()
@@ -997,10 +907,13 @@ window.goNext = ->
findAndFollowRel("next") || findAndFollowLink(nextStrings)
showFindModeHUDForQuery = ->
- if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
- HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Matches)")
- else
+ if findModeQuery.rawQuery and (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
+ plural = if findModeQuery.matchCount == 1 then "" else "es"
+ HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Match#{plural})")
+ else if findModeQuery.rawQuery
HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")
+ else
+ HUD.show("/")
getCurrentRange = ->
selection = getSelection()
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 8faec088..b0fefc7d 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -1,7 +1,6 @@
root = exports ? window
class HandlerStack
-
constructor: ->
@debug = false
@eventNumber = 0
@@ -104,5 +103,9 @@ class HandlerStack
for handler in @stack[..].reverse()
console.log " ", handler._name
+ # For tests only.
+ reset: ->
+ @stack = []
+
root.HandlerStack = HandlerStack
root.handlerStack = new HandlerStack()
diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee
index cdc66e19..5c95680c 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -1,7 +1,7 @@
KeyboardUtils =
keyCodes:
- { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, ctrlEnter: 10, space: 32, shiftKey: 16, ctrlKey: 17,
- f1: 112, f12: 123, tab: 9 }
+ { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, ctrlEnter: 10, space: 32, shiftKey: 16, ctrlKey: 17, f1: 112,
+ f12: 123, tab: 9, downArrow: 40, upArrow: 38 }
keyNames:
{ 37: "left", 38: "up", 39: "right", 40: "down" }
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 0ade7f0e..18a72a37 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -69,18 +69,14 @@ class VomnibarUI
@selection = @initialSelectionValue
updateSelection: ->
- # We have taken the option to add some global state here (previousCompletionType) to tell if a search
- # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1
- # I feel that this approach is cleaner than bubbling the state up from the suggestion level
- # so we just inspect it afterwards
+ # We retain global state here (previousAutoSelect) to tell if a search item (for which autoSelect is set)
+ # has just appeared or disappeared. If that happens, we set @selection to 0 or -1.
if @completions[0]
- if @previousCompletionType != "search" && @completions[0].type == "search"
- @selection = 0
- else if @previousCompletionType == "search" && @completions[0].type != "search"
- @selection = -1
+ @selection = 0 if @completions[0].autoSelect and not @previousAutoSelect
+ @selection = -1 if @previousAutoSelect and not @completions[0].autoSelect
+ @previousAutoSelect = @completions[0].autoSelect
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
- @previousCompletionType = @completions[0].type if @completions[0]
#
# Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee
index ad4ae74b..2e7c6a5a 100644
--- a/tests/dom_tests/chrome.coffee
+++ b/tests/dom_tests/chrome.coffee
@@ -3,6 +3,9 @@
#
root = exports ? window
+root.chromeMessages = []
+
+document.hasFocus = -> true
root.chrome = {
runtime: {
@@ -18,7 +21,7 @@ root.chrome = {
onMessage: {
addListener: ->
}
- sendMessage: ->
+ sendMessage: (message) -> chromeMessages.unshift message
getManifest: ->
getURL: (url) -> "../../#{url}"
}
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index a4713a72..11fbe11f 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -1,29 +1,38 @@
-#
-# Dispatching keyboard events via the DOM would require async tests,
-# which tend to be more complicated. Here we create mock events and
-# invoke the handlers directly.
-#
-mockKeyboardEvent = (keyChar) ->
- event = {}
- event.charCode = (if keyCodes[keyChar] isnt undefined then keyCodes[keyChar] else keyChar.charCodeAt(0))
- event.keyIdentifier = "U+00" + event.charCode.toString(16)
- event.keyCode = event.charCode
- event.stopImmediatePropagation = -> @suppressed = true
- event.preventDefault = -> @suppressed = true
- event
-
-# Some of these tests have side effects on the handler stack and active mode. Therefore, we take backups and
-# restore them on tear down.
-backupStackState = ->
- Mode.backup = Mode.modes[..]
- InsertMode.permanentInstance.exit()
- handlerStack.backup = handlerStack.stack[..]
-restoreStackState = ->
- for mode in Mode.modes
- mode.exit() unless mode in Mode.backup
- Mode.modes = Mode.backup
- InsertMode.permanentInstance.exit()
- handlerStack.stack = handlerStack.backup
+
+# Install frontend event handlers.
+initializeWhenEnabled()
+
+installListener = (element, event, callback) ->
+ element.addEventListener event, (-> callback.apply(this, arguments)), true
+
+# A count of the number of keyboard events received by the page (for the most recently-sent keystroke). E.g.,
+# we expect 3 if the keystroke is passed through (keydown, keypress, keyup), and 0 if it is suppressed.
+pageKeyboardEventCount = 0
+
+sendKeyboardEvent = (key) ->
+ pageKeyboardEventCount = 0
+ response = window.callPhantom
+ request: "keyboard"
+ key: key
+
+# These listeners receive events after the main frontend listeners, and do not receive suppressed events.
+for type in [ "keydown", "keypress", "keyup" ]
+ installListener window, type, (event) ->
+ pageKeyboardEventCount += 1
+
+# Some tests have side effects on the handler stack and the active mode, so these are reset on setup.
+initializeModeState = ->
+ Mode.reset()
+ handlerStack.reset()
+ initializeModes()
+ # We use "m" as the only mapped key, "p" as a passkey, and "u" as an unmapped key.
+ refreshCompletionKeys
+ completionKeys: "mp"
+ handlerStack.bubbleEvent "registerStateChange",
+ enabled: true
+ passKeys: "p"
+ handlerStack.bubbleEvent "registerKeyQueue",
+ keyQueue: ""
#
# Retrieve the hint markers as an array object.
@@ -40,6 +49,7 @@ createGeneralHintTests = (isFilteredMode) ->
context "Link hints",
setup ->
+ initializeModeState()
testContent = "<a>test</a>" + "<a>tress</a>"
document.getElementById("test-div").innerHTML = testContent
stub settings.values, "filterLinkHints", false
@@ -77,6 +87,7 @@ createGeneralHintTests true
context "Alphabetical link hints",
setup ->
+ initializeModeState()
stub settings.values, "filterLinkHints", false
stub settings.values, "linkHintCharacters", "ab"
@@ -99,7 +110,7 @@ context "Alphabetical link hints",
should "narrow the hints", ->
hintMarkers = getHintMarkers()
- LinkHints.onKeyDownInMode hintMarkers, mockKeyboardEvent("A")
+ sendKeyboardEvent "A"
assert.equal "none", hintMarkers[1].style.display
assert.equal "", hintMarkers[0].style.display
@@ -112,6 +123,7 @@ context "Filtered link hints",
context "Text hints",
setup ->
+ initializeModeState()
testContent = "<a>test</a>" + "<a>tress</a>" + "<a>trait</a>" + "<a>track<img alt='alt text'/></a>"
document.getElementById("test-div").innerHTML = testContent
LinkHints.init()
@@ -128,17 +140,18 @@ context "Filtered link hints",
should "narrow the hints", ->
hintMarkers = getHintMarkers()
- LinkHints.onKeyDownInMode hintMarkers, mockKeyboardEvent("T")
- LinkHints.onKeyDownInMode hintMarkers, mockKeyboardEvent("R")
+ sendKeyboardEvent "T"
+ sendKeyboardEvent "R"
assert.equal "none", hintMarkers[0].style.display
assert.equal "1", hintMarkers[1].hintString
assert.equal "", hintMarkers[1].style.display
- LinkHints.onKeyDownInMode hintMarkers, mockKeyboardEvent("A")
+ sendKeyboardEvent "A"
assert.equal "2", hintMarkers[3].hintString
context "Image hints",
setup ->
+ initializeModeState()
testContent = "<a><img alt='alt text'/></a><a><img alt='alt text' title='some title'/></a>
<a><img title='some title'/></a>" + "<a><img src='' width='320px' height='100px'/></a>"
document.getElementById("test-div").innerHTML = testContent
@@ -158,6 +171,7 @@ context "Filtered link hints",
context "Input hints",
setup ->
+ initializeModeState()
testContent = "<input type='text' value='some value'/><input type='password' value='some value'/>
<textarea>some text</textarea><label for='test-input'/>a label</label>
<input type='text' id='test-input' value='some value'/>
@@ -180,39 +194,40 @@ context "Filtered link hints",
context "Input focus",
setup ->
+ initializeModeState()
testContent = "<input type='text' id='first'/><input style='display:none;' id='second'/>
<input type='password' id='third' value='some value'/>"
document.getElementById("test-div").innerHTML = testContent
- backupStackState()
tearDown ->
document.getElementById("test-div").innerHTML = ""
- restoreStackState()
- should "focus the right element", ->
+ should "focus the first element", ->
focusInput 1
assert.equal "first", document.activeElement.id
+ should "focus the nth element", ->
focusInput 100
assert.equal "third", document.activeElement.id
- handlerStack.bubbleEvent 'keydown', mockKeyboardEvent("A")
- # This is the same as above, but also verifies that focusInput activates insert mode.
- should "activate insert mode", ->
+ should "activate insert mode on the first element", ->
focusInput 1
- handlerStack.bubbleEvent 'focus', target: document.activeElement
assert.isTrue InsertMode.permanentInstance.isActive()
+ should "activate insert mode on the first element", ->
focusInput 100
- handlerStack.bubbleEvent 'focus', target: document. activeElement
assert.isTrue InsertMode.permanentInstance.isActive()
- should "select the previously-focused input when count is 1", ->
- focusInput 100
- handlerStack.bubbleEvent 'focus', target: document. activeElement
+ should "activate the most recently-selected input if the count is 1", ->
+ focusInput 3
focusInput 1
assert.equal "third", document.activeElement.id
+ should "not trigger insert if there are no inputs", ->
+ document.getElementById("test-div").innerHTML = ""
+ focusInput 1
+ assert.isFalse InsertMode.permanentInstance.isActive()
+
# TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has
# a tighter contract than goNext(), since they test minor aspects of goNext()'s link matching behavior, and we
# don't need to construct external state many times over just to test that.
@@ -222,6 +237,7 @@ context "Input focus",
context "Find prev / next links",
setup ->
+ initializeModeState()
window.location.hash = ""
should "find exact matches", ->
@@ -278,185 +294,88 @@ createLinks = (n) ->
link.textContent = "test"
document.getElementById("test-div").appendChild link
-# For these tests, we use "m" as a mapped key, "p" as a pass key, and "u" as an unmapped key.
context "Normal mode",
setup ->
- document.activeElement?.blur()
- backupStackState()
- refreshCompletionKeys
- completionKeys: "m"
-
- tearDown ->
- restoreStackState()
+ initializeModeState()
should "suppress mapped keys", ->
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "m"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
+ sendKeyboardEvent "m"
+ assert.equal pageKeyboardEventCount, 0
should "not suppress unmapped keys", ->
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
-context "Passkeys mode",
- setup ->
- backupStackState()
- refreshCompletionKeys
- completionKeys: "mp"
-
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: ""
-
- handlerStack.bubbleEvent "registerKeyQueue",
- keyQueue: ""
-
- tearDown ->
- restoreStackState()
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: ""
+ sendKeyboardEvent "u"
+ assert.equal pageKeyboardEventCount, 3
- handlerStack.bubbleEvent "registerKeyQueue",
- keyQueue: ""
+ should "not suppress escape", ->
+ sendKeyboardEvent "escape"
+ assert.equal pageKeyboardEventCount, 2
should "not suppress passKeys", ->
- # First check normal-mode key (just to verify the framework).
- for k in [ "m", "p" ]
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "p"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
-
- # Install passKey.
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: "p"
-
- # Then verify passKey.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "p"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
- # And re-verify a mapped key.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "m"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
+ sendKeyboardEvent "p"
+ assert.equal pageKeyboardEventCount, 3
should "suppress passKeys with a non-empty keyQueue", ->
- # Install passKey.
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: "p"
-
- # First check the key is indeed not suppressed.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "p"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
- handlerStack.bubbleEvent "registerKeyQueue",
- keyQueue: "1"
-
- # Now verify that the key is suppressed.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "p"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
+ handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "p"
+ sendKeyboardEvent "p"
+ assert.equal pageKeyboardEventCount, 0
context "Insert mode",
setup ->
- document.activeElement?.blur()
- backupStackState()
- refreshCompletionKeys
- completionKeys: "m"
-
- tearDown ->
- backupStackState()
+ initializeModeState()
+ @insertMode = new InsertMode global: true
should "not suppress mapped keys in insert mode", ->
- # First verify normal-mode key (just to verify the framework).
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "m"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
-
- # Install insert mode.
- insertMode = new InsertMode
- global: true
-
- # Then verify insert mode.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "m"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
- insertMode.exit()
-
- # Then verify that insert mode has been successfully removed.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "m"
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
+ sendKeyboardEvent "m"
+ assert.equal pageKeyboardEventCount, 3
+
+ should "exit on escape", ->
+ assert.isTrue @insertMode.modeIsActive
+ sendKeyboardEvent "escape"
+ assert.isFalse @insertMode.modeIsActive
+
+ should "resume normal mode after leaving insert mode", ->
+ @insertMode.exit()
+ sendKeyboardEvent "m"
+ assert.equal pageKeyboardEventCount, 0
context "Triggering insert mode",
setup ->
- document.activeElement?.blur()
- backupStackState()
- refreshCompletionKeys
- completionKeys: "m"
+ initializeModeState()
testContent = "<input type='text' id='first'/>
<input style='display:none;' id='second'/>
- <input type='password' id='third' value='some value'/>"
+ <input type='password' id='third' value='some value'/>
+ <p id='fourth' contenteditable='true'/>
+ <p id='fifth'/>"
document.getElementById("test-div").innerHTML = testContent
tearDown ->
- restoreStackState()
+ document.activeElement?.blur()
document.getElementById("test-div").innerHTML = ""
- should "trigger insert mode on focus of contentEditable elements", ->
- handlerStack.bubbleEvent "focus",
- target:
- isContentEditable: true
-
- assert.isTrue Mode.top().name == "insert" and Mode.top().isActive()
-
should "trigger insert mode on focus of text input", ->
+ assert.isTrue Mode.top().name == "insert" and not Mode.top().isActive()
document.getElementById("first").focus()
- handlerStack.bubbleEvent "focus", { target: document.activeElement }
-
assert.isTrue Mode.top().name == "insert" and Mode.top().isActive()
should "trigger insert mode on focus of password input", ->
+ assert.isTrue Mode.top().name == "insert" and not Mode.top().isActive()
document.getElementById("third").focus()
- handlerStack.bubbleEvent "focus", { target: document.activeElement }
-
assert.isTrue Mode.top().name == "insert" and Mode.top().isActive()
- should "not handle suppressed events", ->
- document.getElementById("first").focus()
- handlerStack.bubbleEvent "focus", { target: document.activeElement }
+ should "trigger insert mode on focus of contentEditable elements", ->
+ assert.isTrue Mode.top().name == "insert" and not Mode.top().isActive()
+ document.getElementById("fourth").focus()
assert.isTrue Mode.top().name == "insert" and Mode.top().isActive()
- for event in [ "keydown", "keypress", "keyup" ]
- # Because "m" is mapped, we expect insert mode to ignore it, and normal mode to suppress it.
- key = mockKeyboardEvent "m"
- InsertMode.suppressEvent key
- handlerStack.bubbleEvent event, key
- assert.isTrue key.suppressed
-
+ should "not trigger insert mode on other elements", ->
+ assert.isTrue Mode.top().name == "insert" and not Mode.top().isActive()
+ document.getElementById("fifth").focus()
+ assert.isTrue Mode.top().name == "insert" and not Mode.top().isActive()
context "Mode utilities",
setup ->
- backupStackState()
- refreshCompletionKeys
- completionKeys: "m"
+ initializeModeState()
testContent = "<input type='text' id='first'/>
<input style='display:none;' id='second'/>
@@ -464,237 +383,152 @@ context "Mode utilities",
document.getElementById("test-div").innerHTML = testContent
tearDown ->
- restoreStackState()
document.getElementById("test-div").innerHTML = ""
should "not have duplicate singletons", ->
count = 0
class Test extends Mode
- constructor: ->
- count += 1
- super
- singleton: Test
-
- exit: ->
- count -= 1
- super()
+ constructor: -> count += 1; super singleton: Test
+ exit: -> count -= 1; super()
assert.isTrue count == 0
for [1..10]
- mode = new Test(); assert.isTrue count == 1
+ mode = new Test()
+ assert.isTrue count == 1
mode.exit()
assert.isTrue count == 0
should "exit on escape", ->
- escape =
- keyCode: 27
-
- new Mode
- exitOnEscape: true
- name: "test"
+ test = new Mode exitOnEscape: true
- assert.isTrue Mode.top().name == "test"
- handlerStack.bubbleEvent "keydown", escape
- assert.isTrue Mode.top().name != "test"
+ assert.isTrue test.modeIsActive
+ sendKeyboardEvent "escape"
+ assert.equal pageKeyboardEventCount, 0
+ assert.isFalse test.modeIsActive
should "not exit on escape if not enabled", ->
- escape =
- keyCode: 27
- keyIdentifier: ""
- stopImmediatePropagation: ->
-
- new Mode
- exitOnEscape: false
- name: "test"
+ test = new Mode exitOnEscape: false
- assert.isTrue Mode.top().name == "test"
- handlerStack.bubbleEvent "keydown", escape
- assert.isTrue Mode.top().name == "test"
+ assert.isTrue test.modeIsActive
+ sendKeyboardEvent "escape"
+ assert.equal pageKeyboardEventCount, 2
+ assert.isTrue test.modeIsActive
should "exit on blur", ->
element = document.getElementById("first")
element.focus()
+ test = new Mode exitOnBlur: element
- new Mode
- exitOnBlur: element
- name: "test"
-
- assert.isTrue Mode.top().name == "test"
- handlerStack.bubbleEvent "blur", { target: element }
- assert.isTrue Mode.top().name != "test"
-
- should "not exit on blur if not enabled", ->
- element = document.getElementById("first")
- element.focus()
+ assert.isTrue test.modeIsActive
+ element.blur()
+ assert.isFalse test.modeIsActive
- new Mode
- exitOnBlur: null
- name: "test"
+ should "not exit on blur if not enabled", ->
+ element = document.getElementById("first")
+ element.focus()
+ test = new Mode exitOnBlur: false
- assert.isTrue Mode.top().name == "test"
- handlerStack.bubbleEvent "blur", { target: element }
- assert.isTrue Mode.top().name == "test"
+ assert.isTrue test.modeIsActive
+ element.blur()
+ assert.isTrue test.modeIsActive
should "register state change", ->
- enabled = null
- passKeys = null
+ test = new Mode trackState: true
+ handlerStack.bubbleEvent "registerStateChange", { enabled: "one", passKeys: "two" }
- class Test extends Mode
- constructor: ->
- super
- trackState: true
+ assert.isTrue test.enabled == "one"
+ assert.isTrue test.passKeys == "two"
- registerStateChange: ->
- enabled = @enabled
- passKeys = @passKeys
-
- new Test()
- handlerStack.bubbleEvent "registerStateChange",
- enabled: "enabled"
- passKeys: "passKeys"
- assert.isTrue enabled == "enabled"
- assert.isTrue passKeys == "passKeys"
+ should "register the keyQueue", ->
+ test = new Mode trackState: true
+ handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "hello"
- should "suppress printable keys", ->
- element = document.getElementById("first")
- element.focus()
- handlerStack.bubbleEvent "focus", { target: document.activeElement }
-
- # Verify that a key is not suppressed.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
- new PostFindMode {}
-
- # Verify that the key is now suppressed for keypress.
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent "keypress",
- extend key,
- srcElement: element
- assert.isTrue key.suppressed
-
- # Verify key is not suppressed with Control key.
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent "keypress",
- extend key,
- srcElement: element
- ctrlKey: true
- assert.isFalse key.suppressed
-
- # Verify key is not suppressed with Meta key.
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent "keypress",
- extend key,
- srcElement: element
- metaKey: true
- assert.isFalse key.suppressed
+ assert.isTrue test.keyQueue == "hello"
context "PostFindMode",
setup ->
- backupStackState()
- refreshCompletionKeys
- completionKeys: "m"
+ initializeModeState()
- testContent = "<input type='text' id='first'/>
- <input style='display:none;' id='second'/>
- <input type='password' id='third' value='some value'/>"
+ testContent = "<input type='text' id='first'/>"
document.getElementById("test-div").innerHTML = testContent
-
- @escape =
- keyCode: 27
- keyIdentifier: ""
- stopImmediatePropagation: ->
- preventDefault: ->
-
- @element = document.getElementById("first")
- @element.focus()
- handlerStack.bubbleEvent "focus", { target: document.activeElement }
+ document.getElementById("first").focus()
+ @postFindMode = new PostFindMode
tearDown ->
- restoreStackState()
document.getElementById("test-div").innerHTML = ""
should "be a singleton", ->
- count = 0
+ assert.isTrue @postFindMode.modeIsActive
+ new PostFindMode
+ assert.isFalse @postFindMode.modeIsActive
- assert.isTrue Mode.top().name == "insert"
- new PostFindMode @element
- assert.isTrue Mode.top().name == "post-find"
- new PostFindMode @element
- assert.isTrue Mode.top().name == "post-find"
- Mode.top().exit()
- assert.isTrue Mode.top().name == "insert"
-
- should "suppress unmapped printable keypress events", ->
- # Verify key is passed through.
- for event in [ "keydown", "keypress", "keyup" ]
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent event, key
- assert.isFalse key.suppressed
-
- new PostFindMode @element
-
- # Verify key is now suppressed for keypress.
- key = mockKeyboardEvent "u"
- handlerStack.bubbleEvent "keypress",
- extend key,
- srcElement: @element
- assert.isTrue key.suppressed
-
- should "be clickable to focus", ->
- new PostFindMode @element
-
- assert.isTrue Mode.top().name != "insert"
- handlerStack.bubbleEvent "click", { target: document.activeElement }
- assert.isTrue Mode.top().name == "insert"
+ should "suppress unmapped printable keys", ->
+ sendKeyboardEvent "m"
+ assert.equal pageKeyboardEventCount, 0
- should "enter insert mode on immediate escape", ->
+ should "be deactivated on click events", ->
+ handlerStack.bubbleEvent "click", target: document.activeElement
+ assert.isFalse @postFindMode.modeIsActive
- new PostFindMode @element
- assert.isTrue Mode.top().name == "post-find"
- handlerStack.bubbleEvent "keydown", @escape
- assert.isTrue Mode.top().name == "insert"
+ should "enter insert mode on immediate escape", ->
+ sendKeyboardEvent "escape"
+ assert.equal pageKeyboardEventCount, 0
+ assert.isFalse @postFindMode.modeIsActive
- should "not enter insert mode on subsequent escape", ->
- new PostFindMode @element
- assert.isTrue Mode.top().name == "post-find"
- handlerStack.bubbleEvent "keydown", mockKeyboardEvent "u"
- handlerStack.bubbleEvent "keydown", @escape
- assert.isTrue Mode.top().name == "post-find"
+ should "not enter insert mode on subsequent escapes", ->
+ sendKeyboardEvent "a"
+ sendKeyboardEvent "escape"
+ assert.isTrue @postFindMode.modeIsActive
context "Mode badges",
setup ->
- backupStackState()
+ initializeModeState()
+ testContent = "<input type='text' id='first'/>"
+ document.getElementById("test-div").innerHTML = testContent
tearDown ->
- restoreStackState()
+ document.getElementById("test-div").innerHTML = ""
- should "have no badge without passKeys", ->
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: ""
+ should "have no badge in normal mode", ->
+ Mode.updateBadge()
+ assert.isTrue chromeMessages[0].badge == ""
- handlerStack.bubbleEvent "updateBadge", badge = { badge: "" }
- assert.isTrue badge.badge == ""
+ should "have an I badge in insert mode by focus", ->
+ document.getElementById("first").focus()
+ assert.isTrue chromeMessages[0].badge == "I"
- should "have no badge with passKeys", ->
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: "p"
+ should "have no badge after leaving insert mode by focus", ->
+ document.getElementById("first").focus()
+ document.getElementById("first").blur()
+ assert.isTrue chromeMessages[0].badge == ""
+
+ should "have an I badge in global insert mode", ->
+ new InsertMode global: true
+ assert.isTrue chromeMessages[0].badge == "I"
+
+ should "have no badge after leaving global insert mode", ->
+ mode = new InsertMode global: true
+ mode.exit()
+ assert.isTrue chromeMessages[0].badge == ""
- handlerStack.bubbleEvent "updateBadge", badge = { badge: "" }
- assert.isTrue badge.badge == ""
+ should "have a ? badge in PostFindMode (immediately)", ->
+ document.getElementById("first").focus()
+ new PostFindMode
+ assert.isTrue chromeMessages[0].badge == "?"
+
+ should "have no badge in PostFindMode (subsequently)", ->
+ document.getElementById("first").focus()
+ new PostFindMode
+ sendKeyboardEvent "a"
+ assert.isTrue chromeMessages[0].badge == ""
should "have no badge when disabled", ->
handlerStack.bubbleEvent "registerStateChange",
enabled: false
passKeys: ""
- new InsertMode()
- handlerStack.bubbleEvent "updateBadge", badge = { badge: "" }
- assert.isTrue badge.badge == ""
+ document.getElementById("first").focus()
+ assert.isTrue chromeMessages[0].badge == ""
diff --git a/tests/dom_tests/phantom_runner.coffee b/tests/dom_tests/phantom_runner.coffee
index d05d9ab4..93218724 100644
--- a/tests/dom_tests/phantom_runner.coffee
+++ b/tests/dom_tests/phantom_runner.coffee
@@ -14,13 +14,23 @@ page.onConsoleMessage = (msg) ->
console.log msg
page.onError = (msg, trace) ->
- console.log(msg);
+ console.log(msg)
trace.forEach (item) ->
console.log(' ', item.file, ':', item.line)
page.onResourceError = (resourceError) ->
console.log(resourceError.errorString)
+page.onCallback = (request) ->
+ switch request.request
+ when "keyboard"
+ switch request.key
+ when "escape"
+ page.sendEvent "keydown", page.event.key.Escape
+ page.sendEvent "keyup", page.event.key.Escape
+ else
+ page.sendEvent "keypress", request.key
+
testfile = path.join(path.dirname(system.args[0]), 'dom_tests.html')
page.open testfile, (status) ->
if status != 'success'