diff --git a/packages/driver/package.json b/packages/driver/package.json
index 216b315fa817..3a743ae6c655 100644
--- a/packages/driver/package.json
+++ b/packages/driver/package.json
@@ -7,7 +7,7 @@
"scripts": {
"start": "../coffee/node_modules/.bin/coffee test/support/server.coffee",
"cypress:open": "node ../../cli/bin/cypress open --dev --project ./test",
- "cypress:run": "node ../../scripts/run-cypress-tests.js --browser chrome --dir test",
+ "cypress:run": "node ../../scripts/run-cypress-tests.js --dir test",
"clean-deps": "rm -rf node_modules"
},
"files": [
@@ -53,9 +53,11 @@
"parse-domain": "2.0.0",
"setimmediate": "^1.0.2",
"sinon": "3.2.0",
+ "text-mask-addons": "^3.7.2",
"underscore": "^1.8.3",
"underscore.string": "3.3.4",
"url-parse": "^1.1.7",
+ "vanilla-text-mask": "^5.1.1",
"wait-on": "^2.0.2",
"zone.js": "^0.8.18"
}
diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee
index 90346e3f0dec..c217e929352a 100644
--- a/packages/driver/src/cy/actionability.coffee
+++ b/packages/driver/src/cy/actionability.coffee
@@ -247,7 +247,7 @@ verify = (cy, $el, options, callbacks) ->
## then do not perform these additional ensures...
if (force isnt true) and (options.waitForAnimations isnt false)
## store the coords that were absolute
- ## from the window or from the viewport for sticky elements
+ ## from the window or from the viewport for sticky elements
## (see https://github.com/cypress-io/cypress/pull/1478)
sticky = !!getStickyEl($el)
diff --git a/packages/driver/src/cy/commands/actions/check.coffee b/packages/driver/src/cy/commands/actions/check.coffee
index 43e79a694735..2c413ffbdcf1 100644
--- a/packages/driver/src/cy/commands/actions/check.coffee
+++ b/packages/driver/src/cy/commands/actions/check.coffee
@@ -4,6 +4,7 @@ Promise = require("bluebird")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
+$elements = require("../../../dom/elements")
checkOrUncheck = (type, subject, values = [], options = {}) ->
## we're not handling conversion of values to strings
@@ -46,13 +47,23 @@ checkOrUncheck = (type, subject, values = [], options = {}) ->
## in the values array?
## or values array is empty
elHasMatchingValue = ($el) ->
- values.length is 0 or $el.val() in values
+ value = $elements.getNativeProp($el.get(0), "value")
+ values.length is 0 or value in values
## blow up if any member of the subject
## isnt a checkbox or radio
checkOrUncheckEl = (el, index) =>
$el = $(el)
+ if not isAcceptableElement($el)
+ node = $dom.stringify($el)
+ word = $utils.plural(options.$el, "contains", "is")
+ phrase = if type is "check" then " and :radio" else ""
+ $utils.throwErrByPath "check_uncheck.invalid_element", {
+ onFail: options._log
+ args: { node, word, phrase, cmd: type }
+ }
+
isElActionable = elHasMatchingValue($el)
if isElActionable
@@ -78,14 +89,6 @@ checkOrUncheck = (type, subject, values = [], options = {}) ->
options._log.snapshot("before", {next: "after"})
- if not isAcceptableElement($el)
- node = $dom.stringify($el)
- word = $utils.plural(options.$el, "contains", "is")
- phrase = if type is "check" then " and :radio" else ""
- $utils.throwErrByPath "check_uncheck.invalid_element", {
- onFail: options._log
- args: { node, word, phrase, cmd: type }
- }
## if the checkbox was already checked
## then notify the user of this note
diff --git a/packages/driver/src/cy/commands/actions/click.coffee b/packages/driver/src/cy/commands/actions/click.coffee
index d61f888def78..f42d0b21608d 100644
--- a/packages/driver/src/cy/commands/actions/click.coffee
+++ b/packages/driver/src/cy/commands/actions/click.coffee
@@ -6,6 +6,8 @@ $Mouse = require("../../../cypress/mouse")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
+$elements = require("../../../dom/elements")
+$selection = require("../../../dom/selection")
$actionability = require("../../actionability")
module.exports = (Commands, Cypress, cy, state, config) ->
@@ -183,7 +185,9 @@ module.exports = (Commands, Cypress, cy, state, config) ->
onReady: ($elToClick, coords) ->
## TODO: get focused through a callback here
$focused = cy.getFocused()
-
+
+ el = $elToClick.get(0)
+
## record the previously focused element before
## issuing the mousedown because browsers may
## automatically shift the focus to the element
@@ -199,11 +203,14 @@ module.exports = (Commands, Cypress, cy, state, config) ->
if domEvents.mouseDown.preventedDefault or not $dom.isAttached($elToClick)
afterMouseDown($elToClick, coords)
else
+ if $elements.isInput(el) or $elements.isTextarea(el) or $elements.isContentEditable(el)
+ if !$elements.isNeedSingleValueChangeInputElement(el)
+ $selection.moveSelectionToEnd(el)
+
## retrieve the first focusable $el in our parent chain
$elToFocus = getFirstFocusableEl($elToClick)
$focused = cy.getFocused()
-
if shouldFireFocusEvent($focused, $elToFocus)
## if our mousedown went through and
## we are focusing a different element
diff --git a/packages/driver/src/cy/commands/actions/select.coffee b/packages/driver/src/cy/commands/actions/select.coffee
index 5b1d29cf2b36..bc2e0755ba78 100644
--- a/packages/driver/src/cy/commands/actions/select.coffee
+++ b/packages/driver/src/cy/commands/actions/select.coffee
@@ -4,6 +4,7 @@ Promise = require("bluebird")
$dom = require("../../../dom")
$utils = require("../../../cypress/utils")
+$elements = require('../../../dom/elements')
newLineRe = /\n/g
@@ -70,7 +71,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
optionsObjects = options.$el.find("option").map((index, el) ->
## push the value in values array if its
## found within the valueOrText
- value = el.value
+ value = $elements.getNativeProp(el, "value")
optEl = $(el)
if value in valueOrText
@@ -101,7 +102,8 @@ module.exports = (Commands, Cypress, cy, state, config) ->
_.each optionsObjects, (obj, index) ->
if obj.text in valueOrText
optionEls.push obj.$el
- values.push(obj.value)
+ objValue = obj.value
+ values.push(objValue)
## if we didnt set multiple to true and
## we have more than 1 option to set then blow up
diff --git a/packages/driver/src/cy/commands/actions/type.coffee b/packages/driver/src/cy/commands/actions/type.coffee
index c862cb5c1648..47a166c2d803 100644
--- a/packages/driver/src/cy/commands/actions/type.coffee
+++ b/packages/driver/src/cy/commands/actions/type.coffee
@@ -4,6 +4,8 @@ Promise = require("bluebird")
moment = require("moment")
$dom = require("../../../dom")
+$elements = require("../../../dom/elements")
+$selection = require("../../../dom/selection")
$Keyboard = require("../../../cypress/keyboard")
$utils = require("../../../cypress/utils")
$actionability = require("../../actionability")
@@ -21,6 +23,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
Commands.addAll({ prevSubject: "element" }, {
type: (subject, chars, options = {}) ->
+ options = _.clone(options)
## allow the el we're typing into to be
## changed by options -- used by cy.clear()
_.defaults(options, {
@@ -61,10 +64,10 @@ module.exports = (Commands, Cypress, cy, state, config) ->
memo
, {}
- options._log = Cypress.log
+ options._log = Cypress.log {
message: [chars, deltaOptions]
$el: options.$el
- consoleProps: ->
+ consoleProps: -> {
"Typed": chars
"Applied To": $dom.getElements(options.$el)
"Options": deltaOptions
@@ -74,6 +77,8 @@ module.exports = (Commands, Cypress, cy, state, config) ->
data: getTableData()
columns: ["typed", "which", "keydown", "keypress", "textInput", "input", "keyup", "change", "modifiers"]
}
+ }
+ }
options._log.snapshot("before", {next: "after"})
@@ -210,27 +215,24 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## consider changing type to a Promise and juggle logging
cy.now("submit", form, {log: false, $el: form})
- dispatchChangeEvent = (id) ->
+ dispatchChangeEvent = (el, id) ->
change = document.createEvent("HTMLEvents")
change.initEvent("change", true, false)
- dispatched = options.$el.get(0).dispatchEvent(change)
+ dispatched = el.dispatchEvent(change)
if id and updateTable
updateTable(id, null, "change", null, dispatched)
- return dispatched
-
needSingleValueChange = ->
- isDate or
- isMonth or
- isWeek or
- isTime or
- ($dom.isType(options.$el, "number") and _.includes(options.chars, "."))
+ return $elements.isNeedSingleValueChangeInputElement(options.$el.get(0))
## see comment in updateValue below
typed = ""
+ isContentEditable = $elements.isContentEditable(options.$el.get(0))
+ isTextarea = $elements.isTextarea(options.$el.get(0))
+
$Keyboard.type({
$el: options.$el
chars: options.chars
@@ -238,17 +240,17 @@ module.exports = (Commands, Cypress, cy, state, config) ->
release: options.release
window: win
- updateValue: (rng, key) ->
+ updateValue: (el, key) ->
+ ## in these cases, the value must only be set after all
+ ## the characters are input because attemping to set
+ ## a partial/invalid value results in the value being
+ ## set to an empty string
if needSingleValueChange()
- ## in these cases, the value must only be set after all
- ## the characters are input because attemping to set
- ## a partial/invalid value results in the value being
- ## set to an empty string
typed += key
if typed is options.chars
- options.$el.val(options.chars)
+ $elements.setNativeProp(el, "value", options.chars)
else
- rng.text(key, "end")
+ $selection.replaceSelectionContents(el, key)
onBeforeType: (totalKeys) ->
## for the total number of keys we're about to
@@ -277,25 +279,35 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## fires only when the 'value'
## of input/text/contenteditable
## changes
- onTypeChange: ->
- ## never fire any change events for contenteditable
- return if options.$el.is("[contenteditable]")
+ onValueChange: (originalText, el) ->
+ ## contenteditable should never be called here.
+ ## only input's and textareas can have change events
+ if changeEvent = state("changeEvent")
+ if !changeEvent(null, true)
+ state("changeEvent", null)
+ return
+
+ state "changeEvent", (id, readOnly) ->
+ changed = $elements.getNativeProp(el, 'value') isnt originalText
+
+ if !readOnly
+ if changed
+ dispatchChangeEvent(el, id)
+ state "changeEvent", null
- state "changeEvent", ->
- dispatchChangeEvent()
- state "changeEvent", null
+ return changed
- onEnterPressed: (changed, id) ->
+ onEnterPressed: (id) ->
## dont dispatch change events or handle
## submit event if we've pressed enter into
## a textarea or contenteditable
- return if options.$el.is("textarea,[contenteditable]")
+ return if isTextarea || isContentEditable
## if our value has changed since our
## element was activated we need to
## fire a change event immediately
- if changed
- dispatchChangeEvent(id)
+ if changeEvent = state("changeEvent")
+ changeEvent(id)
## handle submit event handler here
simulateSubmitHandler()
@@ -315,32 +327,33 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## if it's the body, don't need to worry about focus
return type() if isBody
- cy.now("focused", {log: false, verify: false})
- .then ($focused) ->
- $actionability.verify(cy, options.$el, options, {
- onScroll: ($el, type) ->
- Cypress.action("cy:scrolled", $el, type)
-
- onReady: ($elToClick) ->
- ## if we dont have a focused element
- ## or if we do and its not ourselves
- ## then issue the click
- if not $focused or ($focused and $focused.get(0) isnt options.$el.get(0))
- ## click the element first to simulate focus
- ## and typical user behavior in case the window
- ## is out of focus
- cy.now("click", $elToClick, {
- $el: $elToClick
- log: false
- verify: false
- _log: options._log
- force: true ## force the click, avoid waiting
- timeout: options.timeout
- interval: options.interval
- }).then(type)
- else
- ## don't click, just type
+ $actionability.verify(cy, options.$el, options, {
+ onScroll: ($el, type) ->
+ Cypress.action("cy:scrolled", $el, type)
+
+ onReady: ($elToClick) ->
+ $focused = cy.getFocused()
+
+ ## if we dont have a focused element
+ ## or if we do and its not ourselves
+ ## then issue the click
+ if not $focused or ($focused and $focused.get(0) isnt options.$el.get(0))
+ ## click the element first to simulate focus
+ ## and typical user behavior in case the window
+ ## is out of focus
+ cy.now("click", $elToClick, {
+ $el: $elToClick
+ log: false
+ verify: false
+ _log: options._log
+ force: true ## force the click, avoid waiting
+ timeout: options.timeout
+ interval: options.interval
+ })
+ .then ->
type()
+ else
+ type()
})
handleFocused()
@@ -377,13 +390,15 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## figure out the options which actually change the behavior of clicks
deltaOptions = $utils.filterOutOptions(options)
- options._log = Cypress.log
+ options._log = Cypress.log {
message: deltaOptions
$el: $el
- consoleProps: ->
+ consoleProps: () -> {
"Applied To": $dom.getElements($el)
"Elements": $el.length
"Options": deltaOptions
+ }
+ }
node = $dom.stringify($el)
diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee
index 41793067868a..7201cf8668fc 100644
--- a/packages/driver/src/cypress.coffee
+++ b/packages/driver/src/cypress.coffee
@@ -6,7 +6,6 @@ moment = require("moment")
Promise = require("bluebird")
sinon = require("sinon")
lolex = require("lolex")
-bililiteRange = require("../vendor/bililiteRange")
$dom = require("./dom")
$errorMessages = require("./cypress/error_messages")
@@ -477,7 +476,6 @@ class $Cypress
minimatch: minimatch
sinon: sinon
lolex: lolex
- bililiteRange: bililiteRange
_.extend $Cypress.prototype.$, _.pick($, "Event", "Deferred", "ajax", "get", "getJSON", "getScript", "post", "when")
diff --git a/packages/driver/src/cypress/keyboard.coffee b/packages/driver/src/cypress/keyboard.coffee
index 500d5a8de8f6..e4ef52fbab24 100644
--- a/packages/driver/src/cypress/keyboard.coffee
+++ b/packages/driver/src/cypress/keyboard.coffee
@@ -1,10 +1,12 @@
_ = require("lodash")
Promise = require("bluebird")
-bililiteRange = require("../../vendor/bililiteRange")
-
+$elements = require("../dom/elements")
+$selection = require("../dom/selection")
$Cypress = require("../cypress")
-charsBetweenCurlyBraces = /({.+?})/
+isSingleDigitRe = /^\d$/
+isStartingDigitRe = /^\d/
+charsBetweenCurlyBracesRe = /({.+?})/
# Keyboard event map
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
@@ -71,55 +73,60 @@ $Keyboard = {
}
specialChars: {
- "{selectall}": (el, options) ->
- options.rng.bounds('all').select()
+ "{selectall}": $selection.selectAll
## charCode = 46
## no keyPress
## no textInput
## yes input (if value is actually changed)
"{del}": (el, options) ->
- {rng} = options
- bounds = rng.bounds()
- if @boundsAreEqual(bounds)
- rng.bounds([bounds[0], bounds[0] + 1])
options.charCode = 46
options.keypress = false
options.textInput = false
options.setKey = "{del}"
+
@ensureKey el, null, options, ->
- prev = rng.all()
- rng.text("", "end")
+ bounds = $selection.getSelectionBounds(el)
+
+ if $selection.isCollapsed(el)
+ ## if there's no text selected, delete the prev char
+ ## if deleted char, send the input event
+ options.input = $selection.deleteRightOfCursor(el)
+ return
+
+ ## text is selected, so delete the selection
+ ## contents and send the input event
+ $selection.deleteSelectionContents(el)
+ options.input = true
- ## after applying the {del}
- ## if our text didnt change
- ## dont send the input event
- if prev is rng.all()
- options.input = false
+ return
## charCode = 8
## no keyPress
## no textInput
## yes input (if value is actually changed)
"{backspace}": (el, options) ->
- {rng} = options
- bounds = rng.bounds()
- if @boundsAreEqual(bounds)
- rng.bounds([bounds[0] - 1, bounds[0]])
options.charCode = 8
options.keypress = false
options.textInput = false
options.setKey = "{backspace}"
+
@ensureKey el, null, options, ->
- prev = rng.all()
- rng.text("", "end")
- ## after applying the {backspace}
- ## if our text didnt change
- ## dont send the input event
- if prev is rng.all()
- options.input = false
+ if $selection.isCollapsed(el)
+ ## if there's no text selected, delete the prev char
+ ## if deleted char, send the input event
+ options.input = $selection.deleteLeftOfCursor(el)
+ return
+
+ ## text is selected, so delete the selection
+ ## contents and send the input event
+ $selection.deleteSelectionContents(el)
+ options.input = true
+
+ return
+
## charCode = 27
## no keyPress
## no textInput
@@ -144,84 +151,53 @@ $Keyboard = {
## no input
## yes change (if input is different from last change event)
"{enter}": (el, options) ->
- {rng} = options
options.charCode = 13
options.textInput = false
options.input = false
options.setKey = "{enter}"
@ensureKey el, "\n", options, ->
- rng.insertEOL()
- changed = options.prev isnt rng.all()
- options.onEnterPressed(changed, options.id)
+ $selection.replaceSelectionContents(el, "\n")
+ options.onEnterPressed(options.id)
## charCode = 37
## no keyPress
## no textInput
## no input
"{leftarrow}": (el, options) ->
- {rng} = options
- bounds = rng.bounds()
options.charCode = 37
options.keypress = false
options.textInput = false
options.input = false
options.setKey = "{leftarrow}"
@ensureKey el, null, options, ->
- switch
- when @boundsAreEqual(bounds)
- ## if bounds are equal move the caret
- ## 1 to the left
- left = bounds[0] - 1
- right = left
- when bounds[0] > 0
- ## just set the cursor back to the left
- ## position
- left = bounds[0]
- right = left
- else
- left = 0
- right = 0
-
- rng.bounds([left, right])
+ $selection.moveCursorLeft(el)
- ## charCode = 38
+ ## charCode = 39
## no keyPress
## no textInput
## no input
- "{uparrow}": (el, options) ->
- options.charCode = 38
+ "{rightarrow}": (el, options) ->
+ options.charCode = 39
options.keypress = false
options.textInput = false
options.input = false
- options.setKey = "{uparrow}"
- @ensureKey(el, null, options)
+ options.setKey = "{rightarrow}"
+ @ensureKey el, null, options, ->
+ $selection.moveCursorRight(el)
- ## charCode = 39
+ ## charCode = 38
## no keyPress
## no textInput
## no input
- "{rightarrow}": (el, options) ->
- {rng} = options
- bounds = rng.bounds()
- options.charCode = 39
+ "{uparrow}": (el, options) ->
+ options.charCode = 38
options.keypress = false
options.textInput = false
options.input = false
- options.setKey = "{rightarrow}"
+ options.setKey = "{uparrow}"
@ensureKey el, null, options, ->
- switch
- when @boundsAreEqual(bounds)
- ## if bounds are equal move the caret
- ## 1 to the right
- left = bounds[0] + 1
- right = left
- else
- ## just set the cursor back to the left
- ## position
- right = bounds[1]
- left = right
+ $selection.moveCursorUp(el)
- rng.bounds([left, right])
## charCode = 40
## no keyPress
@@ -233,7 +209,8 @@ $Keyboard = {
options.textInput = false
options.input = false
options.setKey = "{downarrow}"
- @ensureKey(el, null, options)
+ @ensureKey el, null, options, ->
+ $selection.moveCursorDown(el)
}
modifierChars: {
@@ -259,7 +236,7 @@ $Keyboard = {
onEvent: ->
onBeforeEvent: ->
onBeforeType: ->
- onTypeChange: ->
+ onValueChange: ->
onEnterPressed: ->
onNoMatchingSpecialChars: ->
onBeforeSpecialCharAction: ->
@@ -267,79 +244,8 @@ $Keyboard = {
el = options.$el.get(0)
- bililiteRangeSelection = el.bililiteRangeSelection
- rng = bililiteRange(el).bounds("selection")
-
- ## if the value has changed since previously typing, we need to
- ## update the caret position if the value has changed
- if el.prevValue and @expectedValueDoesNotMatchCurrentValue(el.prevValue, rng)
- @moveCaretToEnd(rng)
- el.prevValue = rng.all()
- bililiteRangeSelection = el.bililiteRangeSelection = rng.bounds()
-
- ## store the previous text value
- ## so we know to fire change events
- ## and change callbacks
- options.prev = rng.all()
-
- resetBounds = (start, end) ->
- if start? and end?
- bounds = [start, end]
- else
- len = rng.length()
- bounds = [len, len]
-
- ## resets the bounds to the
- ## end of the element's text
- if not _.isEqual(rng._bounds, bounds)
- el.bililiteRangeSelection = bounds
- rng.bounds(bounds)
-
- ## restore the bounds if our el already has this
- if bililiteRangeSelection
- rng.bounds(bililiteRangeSelection)
- else
- ## native date/moth/datetime/time input types
- ## do not have selectionStart so we have to
- ## manually fix the range on those elements.
- ## we know we need to do that when
- ## el.selectionStart throws or if the element
- ## does not have a selectionStart property
- try
- if "selectionStart" of el
- el.selectionStart
- else
- resetBounds()
- catch
- ## currently if this throws we're likely on
- ## a native input type (number, etc)
- ## and we're just going to take a shortcut here
- ## by figuring out if there is currently a
- ## selection range of the window. whatever that
- ## value is we need to set the range of the el.
- ## now this will fail if there is a PARTIAL range
- ## for instance if our element has value of: 121234
- ## and the selection range is '12' we cannot know
- ## if it is the [0,1] index or the [2,3] index. to
- ## fix this we need to walk forward and backward by
- ## s.modify('extend', 'backward', 'character') until
- ## we can definitely figure out where the selection is
- ## check if this fires selectionchange events. if it does
- ## we may need an option that enables to use to simply
- ## silence these events, or perhaps just TELL US where
- ## to type via the index.
- try
- selection = el.ownerDocument.getSelection().toString()
- index = options.$el.val().indexOf(selection)
- if selection.length and index > -1
- resetBounds(index, selection.length)
- else
- resetBounds()
- catch
- resetBounds()
-
- keys = options.chars.split(charsBetweenCurlyBraces).map (chars) ->
- if charsBetweenCurlyBraces.test(chars)
+ keys = options.chars.split(charsBetweenCurlyBracesRe).map (chars) ->
+ if charsBetweenCurlyBracesRe.test(chars)
## allow special chars and modifiers to be case-insensitive
chars.toLowerCase()
else
@@ -351,18 +257,9 @@ $Keyboard = {
## how keystrokes come into javascript naturally
Promise
.each keys, (key) =>
- @typeChars(el, rng, key, options)
+ @typeChars(el, key, options)
.then =>
- ## if after typing we ended up changing
- ## our value then fire the onTypeChange callback
- if @expectedValueDoesNotMatchCurrentValue(options.prev, rng)
- options.onTypeChange()
-
- ## after typing be sure to clear all ranges
- if sel = options.window.getSelection()
- sel.removeAllRanges()
-
- unless options.release is false
+ if options.release isnt false
@resetModifiers(el, options.window)
countNumIndividualKeyStrokes: (keys) ->
@@ -377,9 +274,8 @@ $Keyboard = {
memo + chars.length
, 0
- typeChars: (el, rng, chars, options) ->
+ typeChars: (el, chars, options) ->
options = _.clone(options)
- options.rng = rng
switch
when @isSpecialChar(chars)
@@ -392,7 +288,7 @@ $Keyboard = {
.resolve @handleModifier(el, chars, options)
.delay(options.delay)
- when charsBetweenCurlyBraces.test(chars)
+ when charsBetweenCurlyBracesRe.test(chars)
## between curly braces, but not a valid special
## char or modifier
allChars = _.keys(@specialChars).concat(_.keys(@modifierChars)).join(", ")
@@ -422,6 +318,7 @@ $Keyboard = {
simulateKey: (el, eventType, key, options) ->
## bail if we've said not to fire this specific event
## in our options
+
return true if options[eventType] is false
key = options.key ? key
@@ -503,54 +400,71 @@ $Keyboard = {
return dispatched
typeKey: (el, key, options) ->
- ## if we have an afterKey value it means
- ## we've typed in prior to this
- if after = options.afterKey
- ## if this afterKey value is no longer the current value
- ## then something has altered the value and we need to
- ## automatically shift the caret to the end like a real browser
- if @expectedValueDoesNotMatchCurrentValue(after, options.rng)
- @moveCaretToEnd(options.rng)
-
@ensureKey el, key, options, ->
- options.updateValue(options.rng, key)
- ## update the selection that's cached on the element
- ## and store the value for comparison in any future typing
- el.bililiteRangeSelection = options.rng.bounds()
- el.prevValue = el.value
+
+ isDigit = isSingleDigitRe.test(key)
+ isNumberInputType = $elements.isInput(el) and $elements.isInputType(el, 'number')
+
+ if isNumberInputType
+ selectionStart = el.selectionStart
+ valueLength = $elements.getNativeProp(el, "value").length
+ isDigitsInText = isStartingDigitRe.test(options.chars)
+ isValidCharacter = key is '.' or (key is '-' and valueLength)
+ prevChar = options.prevChar
+
+ if !isDigit and (isDigitsInText or !isValidCharacter or selectionStart isnt 0)
+ options.prevChar = key
+ return
+
+ ## only type '.' and '-' if it is the first symbol and there already is a value, or if
+ ## '.' or '-' are appended to a digit. If not, value cannot be set.
+ if isDigit and (prevChar is '.' or (prevChar is '-' and !valueLength))
+ options.prevChar = key
+ key = prevChar + key
+
+ options.updateValue(el, key)
ensureKey: (el, key, options, fn) ->
+ _.defaults(options, {
+ prevText: null
+ })
+
options.id = _.uniqueId("char")
- options.beforeKey = options.rng.all()
+ # options.beforeKey = el.value
maybeUpdateValueAndFireInput = =>
## only call this function if we haven't been told not to
if fn and options.onBeforeSpecialCharAction(options.id, options.key) isnt false
+ if not $elements.isContentEditable(el)
+ prevText = $elements.getNativeProp(el, "value")
fn.call(@)
+ if options.prevText is null and not $elements.isContentEditable(el)
+ options.prevText = prevText
+ options.onValueChange(options.prevText, el)
+
@simulateKey(el, "input", key, options)
if @simulateKey(el, "keydown", key, options)
if @simulateKey(el, "keypress", key, options)
if @simulateKey(el, "textInput", key, options)
- ml = el.maxLength
-
+ if $elements.isInput(el) or $elements.isTextarea(el)
+ ml = el.maxLength
+
## maxlength is -1 by default when omitted
## but could also be null or undefined :-/
- if ml is 0 or ml > 0
+ ## only cafe if we are trying to type a key
+ if (ml is 0 or ml > 0 ) and key
## check if we should update the value
## and fire the input event
## as long as we're under maxlength
- if el.value.length < ml
+
+ if $elements.getNativeProp(el, "value").length < ml
maybeUpdateValueAndFireInput()
else
maybeUpdateValueAndFireInput()
- ## store the afterKey value so we know
- ## if something mutates the value between typing keys
- options.afterKey = options.rng.all()
-
@simulateKey(el, "keyup", key, options)
isSpecialChar: (chars) ->
diff --git a/packages/driver/src/dom/elements.coffee b/packages/driver/src/dom/elements.coffee
index c7f1fc04d023..23017a3eb07d 100644
--- a/packages/driver/src/dom/elements.coffee
+++ b/packages/driver/src/dom/elements.coffee
@@ -7,7 +7,161 @@ $utils = require("../cypress/utils")
fixedOrStickyRe = /(fixed|sticky)/
-focusable = "a[href],link[href],button,input,select,textarea,[tabindex],[contenteditable]"
+contentEditable = '[contenteditable]'
+focusable = "a[href],link[href],button,select,[tabindex],input,textarea,#{contentEditable}"
+
+inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/
+canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/
+
+## rules for native methods and props
+## if a setter or getter or function then add a native method
+## if a traversal, don't
+
+descriptor = (klass, prop) ->
+ Object.getOwnPropertyDescriptor(window[klass].prototype, prop)
+
+_getValue = ->
+ switch
+ when isInput(this)
+ descriptor("HTMLInputElement", "value").get
+ when isTextarea(this)
+ descriptor("HTMLTextAreaElement", "value").get
+ when isSelect(this)
+ descriptor("HTMLSelectElement", "value").get
+ else
+ ## is an option element
+ descriptor("HTMLOptionElement", "value").get
+
+
+_setValue = ->
+ switch
+ when isInput(this)
+ descriptor("HTMLInputElement", "value").set
+ when isTextarea(this)
+ descriptor("HTMLTextAreaElement", "value").set
+ when isSelect(this)
+ descriptor("HTMLSelectElement", "value").set
+ else
+ ## is an options element
+ descriptor("HTMLOptionElement", "value").set
+
+
+_setSelectionRange = () ->
+ switch
+ when isInput(this)
+ window.HTMLInputElement.prototype.setSelectionRange
+ when isTextarea(this)
+ window.HTMLTextAreaElement.prototype.setSelectionRange
+
+nativeGetters = {
+ value: _getValue
+ selectionStart: descriptor("HTMLInputElement", "selectionStart").get
+ isContentEditable: descriptor("HTMLElement", "isContentEditable").get
+}
+
+nativeSetters = {
+ value: _setValue
+}
+
+nativeMethods = {
+ createRange: window.document.createRange
+ execCommand: window.document.execCommand
+ getAttribute: window.Element.prototype.getAttribute
+ setSelectionRange: _setSelectionRange
+ modify: window.Selection.prototype.modify
+}
+
+tryCallNativeMethod = ->
+ try
+ callNativeMethod.apply(null, arguments)
+ catch
+ null
+
+callNativeMethod = (obj, fn, args...) ->
+ if not nativeFn = nativeMethods[fn]
+ fns = _.keys(nativeMethods).join(", ")
+ throw new Error("attempted to use a native fn called: #{fn}. Available fns are: #{fns}")
+
+ retFn = nativeFn.apply(obj, args)
+
+ if _.isFunction(retFn)
+ retFn = retFn.apply(obj, args)
+
+ return retFn
+
+getNativeProp = (obj, prop) ->
+ if not nativeProp = nativeGetters[prop]
+ props = _.keys(nativeGetters).join(", ")
+ throw new Error("attempted to use a native getter prop called: #{prop}. Available props are: #{props}")
+
+ retProp = nativeProp.call(obj, prop)
+
+ if _.isFunction(retProp)
+ ## if we got back another function
+ ## then invoke it again
+ retProp = retProp.call(obj, prop)
+
+ return retProp
+
+setNativeProp = (obj, prop, val) ->
+ if not nativeProp = nativeSetters[prop]
+ fns = _.keys(nativeSetters).join(", ")
+ throw new Error("attempted to use a native setter prop called: #{fn}. Available props are: #{fns}")
+
+ retProp = nativeProp.call(obj, val)
+
+ if _.isFunction(retProp)
+ retProp = retProp.call(obj, val)
+
+ return retProp
+
+isNeedSingleValueChangeInputElement = (el) ->
+ if !isInput(el)
+ return false
+
+ return inputTypeNeedSingleValueChangeRe.test(el.type)
+
+## TODO: switch this to not use this
+# getValue = (el) ->
+# return getNativeProp(el, "value")
+
+# if isTextarea(el)
+# return nativeTextareaValueGetter.call(el)
+
+## TODO: switch this to not use this
+# _setValue = (el, val) ->
+# ## sets value for or