# Install frontend event handlers.
installListeners()
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: ""
# Tell Settings that it's been loaded.
Settings.isLoaded = true
# Shoulda.js doesn't support async code, so we try not to use any.
Utils.nextTick = (func) -> func()
#
# Retrieve the hint markers as an array object.
#
getHintMarkers = ->
Array::slice.call document.getElementsByClassName("vimiumHintMarker"), 0
stubSettings = (key, value) -> stub Settings.cache, key, JSON.stringify value
#
# Generate tests that are common to both default and filtered
# link hinting modes.
#
createGeneralHintTests = (isFilteredMode) ->
context "Link hints",
setup ->
initializeModeState()
testContent = "test" + "tress"
document.getElementById("test-div").innerHTML = testContent
stubSettings "filterLinkHints", false
stubSettings "linkHintCharacters", "ab"
tearDown ->
document.getElementById("test-div").innerHTML = ""
should "create hints when activated, discard them when deactivated", ->
linkHints = LinkHints.activateMode()
assert.isFalse not linkHints.hintMarkerContainingDiv?
linkHints.deactivateMode()
assert.isTrue not linkHints.hintMarkerContainingDiv?
should "position items correctly", ->
assertStartPosition = (element1, element2) ->
assert.equal element1.getClientRects()[0].left, element2.getClientRects()[0].left
assert.equal element1.getClientRects()[0].top, element2.getClientRects()[0].top
stub document.body, "style", "static"
linkHints = LinkHints.activateMode()
hintMarkers = getHintMarkers()
assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
linkHints.deactivateMode()
stub document.body.style, "position", "relative"
linkHints = LinkHints.activateMode()
hintMarkers = getHintMarkers()
assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
linkHints.deactivateMode()
createGeneralHintTests false
createGeneralHintTests true
inputs = []
context "Test link hints for focusing input elements correctly",
setup ->
initializeModeState()
testDiv = document.getElementById("test-div")
testDiv.innerHTML = ""
stubSettings "filterLinkHints", false
stubSettings "linkHintCharacters", "ab"
# Every HTML5 input type except for hidden. We should be able to activate all of them with link hints.
inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file",
"image", "month", "number", "password", "radio", "range", "reset", "search", "submit", "tel", "text",
"time", "url", "week"]
for type in inputTypes
input = document.createElement "input"
input.type = type
testDiv.appendChild input
inputs.push input
tearDown ->
document.getElementById("test-div").innerHTML = ""
should "Focus each input when its hint text is typed", ->
for input in inputs
input.scrollIntoView() # Ensure the element is visible so we create a link hint for it.
activeListener = ensureCalled (event) ->
input.blur() if event.type == "focus"
input.addEventListener "focus", activeListener, false
input.addEventListener "click", activeListener, false
LinkHints.activateMode()
[hint] = getHintMarkers().filter (hint) -> input == hint.clickableItem
sendKeyboardEvent char for char in hint.hintString
input.removeEventListener "focus", activeListener, false
input.removeEventListener "click", activeListener, false
context "Alphabetical link hints",
setup ->
initializeModeState()
stubSettings "filterLinkHints", false
stubSettings "linkHintCharacters", "ab"
# Three hints will trigger double hint chars.
createLinks 3
@linkHints = LinkHints.activateMode()
tearDown ->
@linkHints.deactivateMode()
document.getElementById("test-div").innerHTML = ""
should "label the hints correctly", ->
hintMarkers = getHintMarkers()
expectedHints = ["aa", "b", "ab"]
for hint, i in expectedHints
assert.equal hint, hintMarkers[i].hintString
should "narrow the hints", ->
hintMarkers = getHintMarkers()
sendKeyboardEvent "A"
assert.equal "none", hintMarkers[1].style.display
assert.equal "", hintMarkers[0].style.display
context "Filtered link hints",
# Note. In all of these tests, the order of the elements returned by getHintMarkers() may be different from
# the order they are listed in the test HTML content. This is because LinkHints.activateMode() sorts the
# elements.
setup ->
stubSettings "filterLinkHints", true
stubSettings "linkHintNumbers", "0123456789"
context "Text hints",
setup ->
initializeModeState()
testContent = "test" + "tress" + "trait" + "track"
document.getElementById("test-div").innerHTML = testContent
@linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
@linkHints.deactivateMode()
should "label the hints", ->
hintMarkers = getHintMarkers()
expectedMarkers = [1..4].map (m) -> m.toString()
actualMarkers = [0...4].map (i) -> hintMarkers[i].textContent.toLowerCase()
assert.equal expectedMarkers.length, actualMarkers.length
for marker in expectedMarkers
assert.isTrue marker in actualMarkers
should "narrow the hints", ->
hintMarkers = getHintMarkers()
sendKeyboardEvent "T"
sendKeyboardEvent "R"
assert.equal "none", hintMarkers[0].style.display
assert.equal "3", hintMarkers[1].hintString
assert.equal "", hintMarkers[1].style.display
sendKeyboardEvent "A"
assert.equal "1", hintMarkers[3].hintString
context "Image hints",
setup ->
initializeModeState()
testContent = "
" + "
"
document.getElementById("test-div").innerHTML = testContent
@linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
@linkHints.deactivateMode()
should "label the images", ->
hintMarkers = getHintMarkers().map (marker) -> marker.textContent.toLowerCase()
# We don't know the actual hint numbers which will be assigned, so we replace them with "N".
hintMarkers = hintMarkers.map (str) -> str.replace /^[1-4]/, "N"
assert.equal 4, hintMarkers.length
assert.isTrue "N: alt text" in hintMarkers
assert.isTrue "N: some title" in hintMarkers
assert.isTrue "N: alt text" in hintMarkers
assert.isTrue "N" in hintMarkers
context "Input hints",
setup ->
initializeModeState()
testContent = "
a label
a label: "
document.getElementById("test-div").innerHTML = testContent
@linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
@linkHints.deactivateMode()
should "label the input elements", ->
hintMarkers = getHintMarkers()
hintMarkers = getHintMarkers().map (marker) -> marker.textContent.toLowerCase()
# We don't know the actual hint numbers which will be assigned, so we replace them with "N".
hintMarkers = hintMarkers.map (str) -> str.replace /^[0-9]+/, "N"
assert.equal 5, hintMarkers.length
assert.isTrue "N" in hintMarkers
assert.isTrue "N" in hintMarkers
assert.isTrue "N: a label" in hintMarkers
assert.isTrue "N: a label" in hintMarkers
assert.isTrue "N" in hintMarkers
context "Input focus",
setup ->
initializeModeState()
testContent = "
"
document.getElementById("test-div").innerHTML = testContent
tearDown ->
document.getElementById("test-div").innerHTML = ""
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
should "activate insert mode on the first element", ->
focusInput 1
assert.isTrue InsertMode.permanentInstance.isActive()
should "activate insert mode on the first element", ->
focusInput 100
assert.isTrue InsertMode.permanentInstance.isActive()
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.
# i.e. these tests should look something like:
# assert.equal(findLink(html("))[0].href, "first")
# These could then move outside of the dom_tests file.
context "Find prev / next links",
setup ->
initializeModeState()
window.location.hash = ""
should "find exact matches", ->
document.getElementById("test-div").innerHTML = """
nextcorrupted
next page
"""
stubSettings "nextPatterns", "next"
goNext()
assert.equal '#second', window.location.hash
should "match against non-word patterns", ->
document.getElementById("test-div").innerHTML = """
>>
"""
stubSettings "nextPatterns", ">>"
goNext()
assert.equal '#first', window.location.hash
should "favor matches with fewer words", ->
document.getElementById("test-div").innerHTML = """
lorem ipsum next
next!
"""
stubSettings "nextPatterns", "next"
goNext()
assert.equal '#second', window.location.hash
should "find link relation in header", ->
document.getElementById("test-div").innerHTML = """
"""
goNext()
assert.equal '#first', window.location.hash
should "favor link relation to text matching", ->
document.getElementById("test-div").innerHTML = """
next
"""
goNext()
assert.equal '#first', window.location.hash
should "match mixed case link relation", ->
document.getElementById("test-div").innerHTML = """
"""
goNext()
assert.equal '#first', window.location.hash
createLinks = (n) ->
for i in [0...n] by 1
link = document.createElement("a")
link.textContent = "test"
document.getElementById("test-div").appendChild link
context "Normal mode",
setup ->
initializeModeState()
should "suppress mapped keys", ->
sendKeyboardEvent "m"
assert.equal pageKeyboardEventCount, 0
should "not suppress unmapped keys", ->
sendKeyboardEvent "u"
assert.equal pageKeyboardEventCount, 3
should "not suppress escape", ->
sendKeyboardEvent "escape"
assert.equal pageKeyboardEventCount, 2
should "not suppress passKeys", ->
sendKeyboardEvent "p"
assert.equal pageKeyboardEventCount, 3
should "suppress passKeys with a non-empty keyQueue", ->
handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "p"
sendKeyboardEvent "p"
assert.equal pageKeyboardEventCount, 0
context "Insert mode",
setup ->
initializeModeState()
@insertMode = new InsertMode global: true
should "not suppress mapped keys in insert mode", ->
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 ->
initializeModeState()
testContent = "