diff options
| -rw-r--r-- | background_scripts/commands.coffee | 2 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 3 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 5 | ||||
| -rw-r--r-- | background_scripts/sync.coffee | 38 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 12 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 2 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 4 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 271 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 5 | ||||
| -rw-r--r-- | lib/keyboard_utils.coffee | 4 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 14 | ||||
| -rw-r--r-- | tests/dom_tests/chrome.coffee | 5 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 542 | ||||
| -rw-r--r-- | tests/dom_tests/phantom_runner.coffee | 12 | 
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' | 
