try 2: fix type when previous selection in input in some cases (#5854)

* fix type when previous selection in input

* cleanup

* cleanup more

* more cleanup

* more more cleanup final

* fix not firing input event in all cases
This commit is contained in:
Ben Kucera
2019-12-03 09:59:07 -05:00
committed by Jennifer Shehane
parent 02515fec61
commit 74006acf66
5 changed files with 535 additions and 192 deletions

View File

@@ -8,7 +8,6 @@ import * as $dom from '../dom'
import * as $document from '../dom/document'
import * as $elements from '../dom/elements'
import * as $selection from '../dom/selection'
import { HTMLTextLikeElement, HTMLTextLikeInputElement } from '../dom/types'
import $window from '../dom/window'
const debug = Debug('cypress:driver:keyboard')
@@ -36,7 +35,7 @@ interface KeyDetailsPartial extends Partial<KeyDetails> {
}
type SimulatedDefault = (
el: HTMLTextLikeElement,
el: HTMLElement,
key: KeyDetails,
options: any
) => void
@@ -63,6 +62,7 @@ const monthRe = /^\d{4}-(0\d|1[0-2])/
const weekRe = /^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])/
const timeRe = /^([0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?(\.[0-9]{1,3})?/
const dateTimeRe = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/
const numberRe = /^-?(0|[1-9]\d*)(\.\d+)?(e-?(0|[1-9]\d*))?$/i
const charsBetweenCurlyBracesRe = /({.+?})/
const INITIAL_MODIFIERS = {
@@ -235,7 +235,7 @@ const shouldIgnoreEvent = <
return options[eventName] === false
}
const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => {
const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options) => {
if (!key.text) return false
const bounds = $selection.getSelectionBounds(el)
@@ -246,6 +246,29 @@ const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => {
return false
}
const isNumberInputType = $elements.isInput(el) && $elements.isInputType(el, 'number')
if (isNumberInputType) {
const needsValue = options.prevVal || ''
const needsValueLength = (needsValue && needsValue.length) || 0
const curVal = $elements.getNativeProp(el, 'value')
const bounds = $selection.getSelectionBounds(el)
// We need to see if the number we're about to type is a valid number, since setting a number input
// to an invalid number will not set the value and possibly throw a warning in the console
const potentialValue = $selection.insertSubstring(curVal + needsValue, key.text, [bounds.start + needsValueLength, bounds.end + needsValueLength])
if (!(numberRe.test(potentialValue))) {
debug('skipping inserting value since number input would be invalid', key.text, potentialValue)
options.prevVal = needsValue + key.text
return
}
key.text = (options.prevVal || '') + key.text
options.prevVal = null
}
if (noneSelected) {
const ml = $elements.getNativeProp(el, 'maxLength')
@@ -308,13 +331,15 @@ const validateTyping = (
let isWeek = false
let isDateTime = false
// use 'type' attribute instead of prop since browsers without
// support for attribute input type will have type prop of 'text'
if ($elements.isInput(el)) {
isDate = $dom.isInputType(el, 'date')
isTime = $dom.isInputType(el, 'time')
isMonth = $dom.isInputType(el, 'month')
isWeek = $dom.isInputType(el, 'week')
isDate = $elements.isAttrType(el, 'date')
isTime = $elements.isAttrType(el, 'time')
isMonth = $elements.isAttrType(el, 'month')
isWeek = $elements.isAttrType(el, 'week')
isDateTime =
$dom.isInputType(el, 'datetime') || $dom.isInputType(el, 'datetime-local')
$elements.isAttrType(el, 'datetime') || $elements.isAttrType(el, 'datetime-local')
}
const isFocusable = $elements.isFocusable($el)
@@ -452,11 +477,7 @@ function _getEndIndex (str, substr) {
// Simulated default actions for select few keys.
const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = {
Enter: (el, key, options) => {
if ($elements.isContentEditable(el) || $elements.isTextarea(el)) {
key.events.input = $selection.replaceSelectionContents(el, '\n')
} else {
key.events.input = false
}
$selection.replaceSelectionContents(el, '\n')
options.onEnterPressed()
},
@@ -694,7 +715,7 @@ export class Keyboard {
debug('setting element value', valToSet, activeEl)
return $elements.setNativeProp(
activeEl as HTMLTextLikeInputElement,
activeEl as $elements.HTMLTextLikeInputElement,
'value',
valToSet
)
@@ -954,9 +975,11 @@ export class Keyboard {
const key = this.getModifierKeyDetails(_key)
if (!key.text) {
key.events.input = false
key.events.keypress = false
key.events.textInput = false
if (key.key !== 'Backspace' && key.key !== 'Delete') {
key.events.input = false
}
}
let elToType
@@ -976,9 +999,12 @@ export class Keyboard {
if (key.key === 'Enter' && $elements.isInput(elToType)) {
key.events.textInput = false
key.events.input = false
}
if ($elements.isReadOnlyInputOrTextarea(elToType)) {
if ($elements.isContentEditable(elToType)) {
key.events.input = false
} else if ($elements.isReadOnlyInputOrTextarea(elToType)) {
key.events.textInput = false
}
@@ -1035,7 +1061,7 @@ export class Keyboard {
this.fireSimulatedEvent(el, 'keyup', key, options)
}
getSimulatedDefaultForKey (key: KeyDetails) {
getSimulatedDefaultForKey (key: KeyDetails, options) {
debug('getSimulatedDefaultForKey', key.key)
if (key.simulatedDefault) return key.simulatedDefault
@@ -1044,7 +1070,7 @@ export class Keyboard {
}
return (el: HTMLElement) => {
if (!shouldUpdateValue(el, key)) {
if (!shouldUpdateValue(el, key, options)) {
debug('skip typing key', false)
key.events.input = false
@@ -1072,7 +1098,7 @@ export class Keyboard {
performSimulatedDefault (el: HTMLElement, key: KeyDetails, options: any) {
debug('performSimulatedDefault', key.key)
const simulatedDefault = this.getSimulatedDefaultForKey(key)
const simulatedDefault = this.getSimulatedDefaultForKey(key, options)
if ($elements.isTextLike(el)) {
if ($elements.isInput(el) || $elements.isTextarea(el)) {
@@ -1087,6 +1113,8 @@ export class Keyboard {
simulatedDefault(el, key, options)
}
debug({ key })
shouldIgnoreEvent('input', key.events) ||
this.fireSimulatedEvent(el, 'input', key, options)

View File

@@ -193,6 +193,9 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
contentWindow.SVGElement.prototype.blur = ->
focused.interceptBlur(@)
contentWindow.HTMLInputElement.prototype.select = ->
$selection.interceptSelect.call(@)
contentWindow.document.hasFocus = ->
focused.documentHasFocus.call(@)

View File

@@ -505,6 +505,12 @@ const isInputType = function (el: JQueryOrEl<HTMLElement>, type) {
return elType === type
}
const isAttrType = function (el: HTMLInputElement, type: string) {
const elType = (el.getAttribute('type') || '').toLowerCase()
return elType === type
}
const isScrollOrAuto = (prop) => {
return prop === 'scroll' || prop === 'auto'
}
@@ -1079,6 +1085,7 @@ export {
isIframe,
isTextarea,
isInputType,
isAttrType,
isFocused,
isFocusedOrInFocused,
isInputAllowingImplicitFormSubmission,

View File

@@ -1,8 +1,11 @@
import _ from 'lodash'
import * as $dom from '../dom'
import * as $document from './document'
import * as $elements from './elements'
const debug = require('debug')('cypress:driver:selection')
const INTERNAL_STATE = '__Cypress_state__'
const _getSelectionBoundsFromTextarea = (el) => {
return {
start: $elements.getNativeProp(el, 'selectionStart'),
@@ -18,12 +21,18 @@ const _getSelectionBoundsFromInput = function (el) {
}
}
const doc = $document.getDocumentFromElement(el)
const range = _getSelectionRange(doc)
const internalState = el[INTERNAL_STATE]
if (internalState) {
return {
start: internalState.start,
end: internalState.end,
}
}
return {
start: range.startOffset,
end: range.endOffset,
start: 0,
end: 0,
}
}
@@ -58,9 +67,11 @@ const _getSelectionBoundsFromContentEditable = function (el) {
}
// TODO get ACTUAL caret position in contenteditable, not line
const _replaceSelectionContentsWithExecCommand = function (doc, text) {
const _replaceSelectionContentsContentEditable = function (el, text) {
const doc = $document.getDocumentFromElement(el)
// NOTE: insertText will also handle '\n', and render newlines
return $elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text)
$elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text)
}
// Keeping around native implementation
@@ -102,7 +113,7 @@ const _replaceSelectionContentsWithExecCommand = function (doc, text) {
// # startNode.nodeValue = updatedValue
// el.normalize()
const _insertSubstring = (curText, newText, [start, end]) => {
const insertSubstring = (curText, newText, [start, end]) => {
return curText.substring(0, start) + newText + curText.substring(end)
}
@@ -128,93 +139,123 @@ const getHostContenteditable = function (el) {
return curEl
}
/**
*
* @param {HTMLElement} el
* @returns {Selection}
*/
const _getSelectionByEl = function (el) {
const doc = $document.getDocumentFromElement(el)
return doc.getSelection()!
}
const deleteSelectionContents = function (el: HTMLElement) {
const deleteSelectionContents = function (el) {
if ($elements.isContentEditable(el)) {
const doc = $document.getDocumentFromElement(el)
$elements.callNativeMethod(doc, 'execCommand', 'delete', false, null)
return false
return
}
return replaceSelectionContents(el, '')
}
const setSelectionRange = function (el, start, end) {
$elements.callNativeMethod(el, 'setSelectionRange', start, end)
}
// Whether or not the selection contains any text
// since Selection.isCollapsed will be true when selection
// is inside non-selectionRange input (e.g. input[type=email])
const isSelectionCollapsed = function (selection: Selection) {
return !selection.toString()
}
const deleteRightOfCursor = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
$elements.callNativeMethod(el, 'setSelectionRange', start, end)
return
}
// NOTE: Some input elements have mobile implementations
// and thus may not always have a cursor, so calling setSelectionRange will throw.
// we are assuming desktop here, so we store our own internal state.
el[INTERNAL_STATE] = {
start,
end,
}
}
/**
* @returns {boolean} whether or not input events are needed
*/
const deleteRightOfCursor = function (el) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start === $elements.getNativeProp(el, 'value').length) {
// nothing to delete, nothing to right of selection
return false
}
if (start === end) {
setSelectionRange(el, start, end + 1)
}
return deleteSelectionContents(el)
deleteSelectionContents(el)
// successful delete, needs input events
return true
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'forward',
'character'
)
$elements.callNativeMethod(selection, 'modify', 'extend', 'forward', 'character')
if ($elements.getNativeProp(selection, 'isCollapsed')) {
// there's nothing to delete
return false
}
deleteSelectionContents(el)
// successful delete, does not need input events
return false
}
deleteSelectionContents(el)
return false
}
/**
* @returns {boolean} whether or not input events are needed
*/
const deleteLeftOfCursor = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
debug('delete left of cursor input/textarea', start, end)
if (start === end) {
if (start === 0) {
// there's nothing to delete, nothing before cursor
return false
}
setSelectionRange(el, start - 1, end)
}
return deleteSelectionContents(el)
deleteSelectionContents(el)
// successful delete
return true
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
// there is no 'backwardDelete' command for execCommand, so use the Selection API
const selection = _getSelectionByEl(el)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'backward',
'character'
)
$elements.callNativeMethod(selection, 'modify', 'extend', 'backward', 'character')
if (selection.isCollapsed) {
// there's nothing to delete
// since extending the selection didn't do anything
return false
}
deleteSelectionContents(el)
return false
}
deleteSelectionContents(el)
return false
}
@@ -223,7 +264,7 @@ const _collapseInputOrTextArea = (el, toIndex) => {
}
const moveCursorLeft = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start !== end) {
@@ -237,17 +278,17 @@ const moveCursorLeft = function (el) {
return setSelectionRange(el, start - 1, start - 1)
}
// if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
'move',
'backward',
'character'
)
// }
if (selection.isCollapsed) {
return $elements.callNativeMethod(selection, 'modify', 'move', 'backward', 'character')
}
selection.collapseToStart()
return
}
}
// Keeping around native implementation
@@ -263,7 +304,7 @@ const moveCursorLeft = function (el) {
// range.setEnd(range.startContainer, newOffset)
const moveCursorRight = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start !== end) {
@@ -275,15 +316,11 @@ const moveCursorRight = function (el) {
return setSelectionRange(el, start + 1, end + 1)
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
'move',
'forward',
'character'
)
return $elements.callNativeMethod(selection, 'modify', 'move', 'forward', 'character')
}
}
const moveCursorUp = (el) => {
@@ -314,16 +351,15 @@ const _moveCursorUpOrDown = function (el, up) {
return
}
if ($elements.isTextarea(el) || $elements.isContentEditable(el)) {
const isTextarea = $elements.isTextarea(el)
if (isTextarea || $elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
return $elements.callNativeMethod(selection, 'modify',
'move',
up ? 'backward' : 'forward',
'line'
)
'line')
}
}
@@ -335,8 +371,12 @@ const moveCursorToLineEnd = (el) => {
return _moveCursorToLineStartOrEnd(el, false)
}
const _moveCursorToLineStartOrEnd = function (el, toStart) {
if ($elements.isContentEditable(el) || $elements.isInput(el) || $elements.isTextarea(el)) {
const _moveCursorToLineStartOrEnd = function (el: HTMLElement, toStart) {
const isInput = $elements.isInput(el)
const isTextarea = $elements.isTextarea(el)
const isInputOrTextArea = isInput || isTextarea
if ($elements.isContentEditable(el) || isInputOrTextArea) {
const selection = _getSelectionByEl(el)
// the selection.modify API is non-standard, may work differently in other browsers, and is not in IE11.
@@ -345,34 +385,36 @@ const _moveCursorToLineStartOrEnd = function (el, toStart) {
}
}
const isCollapsed = (el: HTMLElement) => {
if ($elements.canSetSelectionRangeElement(el)) {
const isCollapsed = function (el) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
return start === end
}
const doc = $document.getDocumentFromElement(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return _getSelectionRange(doc).collapsed
return selection.isCollapsed
}
return false
}
const selectAll = function (doc) {
const el = _getActive(doc)
const el = doc.activeElement
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
setSelectionRange(el, 0, $elements.getNativeProp(el, 'value').length)
return
}
return $elements.callNativeMethod(
doc,
'execCommand',
'selectAll',
false,
null
)
if ($elements.isContentEditable(el)) {
const doc = $document.getDocumentFromElement(el)
return $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
}
}
// Keeping around native implementation
// for same reasons as listed below
@@ -408,9 +450,9 @@ const _moveSelectionTo = function (toStart: boolean, doc: Document, options = {}
onlyIfEmptySelection: false,
})
const el = _getActive(doc)
const el = $elements.getActiveElByDocument(doc)
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isInput(el) || $elements.isTextarea(el)) {
if (opts.onlyIfEmptySelection) {
const { start, end } = getSelectionBounds(el)
@@ -430,48 +472,52 @@ const _moveSelectionTo = function (toStart: boolean, doc: Document, options = {}
return
}
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
const selection = doc.getSelection()
if ($elements.isContentEditable(el)) {
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
const selection = doc.getSelection()
if (!selection) {
return
}
// collapsing the range doesn't work on input/textareas, since the range contains more than the input element
// However, IE can always* set selection range, so only modern browsers (with the selection API) will need this
const direction = toStart ? 'backward' : 'forward'
selection.modify('move', direction, 'line')
if (!selection) {
return
}
// collapsing the range doesn't work on input/textareas, since the range contains more than the input element
// However, IE can always* set selection range, so only modern browsers (with the selection API) will need this
const direction = toStart ? 'backward' : 'forward'
selection.modify('move', direction, 'line')
return false
}
const moveSelectionToEnd = _.curry(_moveSelectionTo)(false)
const moveSelectionToStart = _.curry(_moveSelectionTo)(true)
const replaceSelectionContents = function (el: HTMLElement, key: string) {
if ($elements.canSetSelectionRangeElement(el)) {
// if ($elements.isRead)
const replaceSelectionContents = function (el, key) {
if ($elements.isContentEditable(el)) {
_replaceSelectionContentsContentEditable(el, key)
return
}
if ($elements.isInput(el) || $elements.isTextarea(el)) {
const { start, end } = getSelectionBounds(el)
const value = $elements.getNativeProp(el, 'value') || ''
const updatedValue = _insertSubstring(value, key, [start, end])
if (value === updatedValue) {
return false
}
const updatedValue = insertSubstring(value, key, [start, end])
debug(`inserting at selection ${JSON.stringify({ start, end })}`, 'rewriting value to ', updatedValue)
$elements.setNativeProp(el, 'value', updatedValue)
setSelectionRange(el, start + key.length, start + key.length)
return true
return
}
const doc = $document.getDocumentFromElement(el)
_replaceSelectionContentsWithExecCommand(doc, key)
return false
}
const getCaretPosition = function (el) {
@@ -489,28 +535,12 @@ const getCaretPosition = function (el) {
return null
}
const _getActive = function (doc) {
// TODO: remove this state access
// eslint-disable-next-line
const activeEl = $elements.getNativeProp(doc, 'activeElement')
return activeEl
}
const focusCursor = function (el, doc) {
const elToFocus = $elements.getFirstFocusableEl($dom.wrap(el)).get(0)
const prevFocused = _getActive(doc)
elToFocus.focus()
if ($elements.isInput(elToFocus) || $elements.isTextarea(elToFocus)) {
moveSelectionToEnd(doc)
const interceptSelect = function () {
if ($elements.isInput(this) && !$elements.canSetSelectionRangeElement(this)) {
setSelectionRange(this, 0, $elements.getNativeProp(this, 'value').length)
}
if ($elements.isContentEditable(elToFocus) && prevFocused !== elToFocus) {
moveSelectionToEnd(doc)
}
return $elements.callNativeMethod(this, 'select')
}
// Selection API implementation of insert newline.
@@ -606,5 +636,6 @@ export {
moveCursorToLineEnd,
replaceSelectionContents,
isCollapsed,
focusCursor,
insertSubstring,
interceptSelect,
}

View File

@@ -1,7 +1,14 @@
const $ = Cypress.$.bind(Cypress)
const { _ } = Cypress
const { Promise } = Cypress
const { getCommandLogWithText, findReactInstance, withMutableReporterState, attachListeners, shouldBeCalledWithCount } = require('../../../support/utils')
const { getCommandLogWithText,
findReactInstance,
withMutableReporterState,
attachListeners,
shouldBeCalledWithCount,
shouldBeCalledOnce,
shouldNotBeCalled,
} = require('../../../support/utils')
const keyEvents = [
'keydown',
@@ -985,6 +992,49 @@ describe('src/cy/commands/actions/type', () => {
})
})
// https://github.com/cypress-io/cypress/issues/5703
it('overwrites text when selectAll in focus handler', () => {
const input = cy.$$('#input-without-value')
input
.val('f')
.on('focus', (e) => {
e.currentTarget.select()
})
cy.get('#input-without-value')
.type('foo')
.should('have.value', 'foo')
})
it('overwrites text when selectAll in focus handler in number', () => {
const input = cy.$$('#number-without-value')
input
.val('1')
.on('focus', (e) => {
e.currentTarget.select()
})
cy.get('#number-without-value')
.type('10')
.should('have.value', '10')
})
it('overwrites text when selectAll in focus handler in email', () => {
const input = cy.$$('#email-without-value')
input
.val('b')
.on('focus', (e) => {
e.currentTarget.select()
})
cy.get('#email-without-value')
.type('b@foo.com')
.should('have.value', 'b@foo.com')
})
it('overwrites text when selectAll in mouseup handler', () => {
cy.$$('#input-without-value').val('0').mouseup(function () {
$(this).select()
@@ -1570,19 +1620,33 @@ describe('src/cy/commands/actions/type', () => {
})
it('inserts text with only one input event', () => {
const onInput = cy.stub()
const onTextInput = cy.stub()
const ce = cy.$$('#input-types [contenteditable]')
attachKeyListeners({ ce })
cy.get('#input-types [contenteditable]')
.invoke('text', 'foo')
.then(($el) => $el.on('input', onInput))
.then(($el) => $el.on('input', onTextInput))
.type('\n').then(($text) => {
.type('f')
.should(($text) => {
expect(trimInnerText($text)).eq('foof')
})
cy.getAll('ce', 'keydown keypress textInput input keyup').each(shouldBeCalledOnce)
})
it('{enter} inserts text with only one input event', () => {
const ce = cy.$$('#input-types [contenteditable]')
attachKeyListeners({ ce })
cy.get('#input-types [contenteditable]')
.invoke('text', 'foo')
.type('{enter}')
.should(($text) => {
expect(trimInnerText($text)).eq('foo')
})
.then(() => expect(onInput).to.be.calledOnce)
.then(() => expect(onTextInput).to.be.calledOnce)
cy.getAll('ce', 'keydown keypress textInput input keyup').each(shouldBeCalledOnce)
})
it('can type into [contenteditable] with existing <div>', () => {
@@ -1910,6 +1974,92 @@ describe('src/cy/commands/actions/type', () => {
done()
})
})
it('correct events in input', () => {
const input = cy.$$(':text:first')
attachKeyListeners({ input })
cy.get(':text:first').invoke('val', 'ab')
.focus()
.type('{backspace}')
.should('have.value', 'a')
cy.getAll('input', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput').each(shouldNotBeCalled)
})
it('correct events in input when noop', () => {
const input = cy.$$(':text:first')
attachKeyListeners({ input })
cy.get(':text:first').invoke('val', 'ab')
.then(($input) => $input[0].setSelectionRange(0, 0))
.focus()
.type('{backspace}')
.should('have.value', 'ab')
cy.getAll('input', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput input').each(shouldNotBeCalled)
})
it('correct events in textarea', () => {
const textarea = cy.$$('textarea:first')
attachKeyListeners({ textarea })
cy.get('textarea:first').invoke('val', 'ab')
.focus()
.type('{backspace}')
.should('have.value', 'a')
cy.getAll('textarea', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('textarea', 'keypress textInput').each(shouldNotBeCalled)
})
it('correct events in textarea when noop', () => {
const input = cy.$$('textarea:first')
attachKeyListeners({ input })
cy.get('textarea:first').invoke('val', 'ab')
.then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.focus()
.type('{backspace}')
.should('have.value', 'ab')
cy.getAll('input', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput input').each(shouldNotBeCalled)
})
it('correct events in contenteditable', () => {
const ce = cy.$$('[contenteditable]:first')
attachKeyListeners({ ce })
cy.get('[contenteditable]:first').invoke('text', 'ab')
.scrollIntoView()
.type('{backspace}')
.should('have.text', 'a')
cy.getAll('ce', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('ce', 'keypress textInput').each(shouldNotBeCalled)
})
it('correct events in contenteditable when noop', () => {
const ce = cy.$$('[contenteditable]:first')
attachKeyListeners({ ce })
cy.get('[contenteditable]:first').invoke('text', 'ab')
.focus()
.type('{backspace}')
.should('have.text', 'ab')
cy.getAll('ce', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('ce', 'keypress textInput input').each(shouldNotBeCalled)
})
})
context('{del}', () => {
@@ -1945,37 +2095,107 @@ describe('src/cy/commands/actions/type', () => {
cy.get(':text:first').invoke('val', 'ab').type('{leftarrow}{del}')
})
it('does not fire textInput event', (done) => {
cy.$$(':text:first').on('textInput', (e) => {
done('textInput should not have fired')
})
it('correct events in input', () => {
const input = cy.$$(':text:first')
cy.get(':text:first').invoke('val', 'ab').type('{del}').then(() => {
done()
})
attachKeyListeners({ input })
cy.get(':text:first').invoke('val', 'ab')
.then(($input) => $input[0].setSelectionRange(0, 0))
.focus()
.type('{del}')
.should('have.value', 'b')
cy.getAll('input', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput').each(shouldNotBeCalled)
})
it('{del} does fire input event when value changes', () => {
const onInput = cy.stub()
it('correct events in input when noop', () => {
const input = cy.$$(':text:first')
cy.$$(':text:first').on('input', onInput)
attachKeyListeners({ input })
// select the 'a' characters
cy
.get(':text:first').invoke('val', 'bar').focus().then(($input) => {
$input.get(0).setSelectionRange(0, 1)
}).get(':text:first').type('{del}')
.then(() => expect(onInput).to.be.calledOnce)
cy.get(':text:first').invoke('val', 'ab')
.focus()
.type('{del}')
.should('have.value', 'ab')
cy.getAll('input', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput input').each(shouldNotBeCalled)
})
it('does not fire input event when value does not change', (done) => {
cy.$$(':text:first').on('input', (e) => {
done('should not have fired input')
it('correct events in textarea', () => {
const textarea = cy.$$('textarea:first')
attachKeyListeners({ textarea })
cy.get('textarea:first').invoke('val', 'ab')
.then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.focus()
.type('{del}')
.should('have.value', 'b')
cy.getAll('textarea', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('textarea', 'keypress textInput').each(shouldNotBeCalled)
})
it('correct events in textarea when noop', () => {
const input = cy.$$('textarea:first')
attachKeyListeners({ input })
cy.get('textarea:first').invoke('val', 'ab')
.scrollIntoView()
.type('{del}')
.should('have.value', 'ab')
cy.getAll('input', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput input').each(shouldNotBeCalled)
})
it('correct events in contenteditable', () => {
const ce = cy.$$('[contenteditable]:first')
const keydown = cy.stub().callsFake((e) => {
expect(e.which).to.eq(46)
expect(e.keyCode).to.eq(46)
expect(e.key).to.eq('Delete')
})
cy.get(':text:first').invoke('val', 'ab').type('{del}').then(() => {
done()
ce.on('keydown', keydown)
attachKeyListeners({ ce })
cy.get('[contenteditable]:first').invoke('text', 'ab')
.focus()
.type('{del}')
.should('have.text', 'b')
cy.getAll('ce', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('ce', 'keypress textInput').each(shouldNotBeCalled)
})
it('correct events in contenteditable when noop', () => {
const ce = cy.$$('[contenteditable]:first')
const keydown = cy.stub().callsFake((e) => {
expect(e.which).to.eq(46)
expect(e.keyCode).to.eq(46)
expect(e.key).to.eq('Delete')
})
ce.on('keydown', keydown)
attachKeyListeners({ ce })
cy.get('[contenteditable]:first').invoke('text', 'ab')
.scrollIntoView()
.type('{del}')
.should('have.text', 'ab')
cy.getAll('ce', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('ce', 'keypress textInput input').each(shouldNotBeCalled)
})
it('can prevent default del movement', (done) => {
@@ -2543,14 +2763,68 @@ describe('src/cy/commands/actions/type', () => {
})
})
it('does not fire textInput event', (done) => {
cy.$$(':text:first').on('textInput', (e) => {
done('textInput should not have fired')
it('{enter} correct events in input', () => {
const input = cy.$$(':text:first')
attachKeyListeners({ input })
cy.get(':text:first')
.invoke('val', 'ab')
.type('{enter}')
.should('have.value', 'ab')
cy.getAll('input', 'keydown keypress keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'textInput input').each(shouldNotBeCalled)
})
it('{enter} correct events in [contenteditable]', () => {
const ce = cy.$$('[contenteditable]:first')
attachKeyListeners({ ce })
cy.get('[contenteditable]:first')
.focus()
.invoke('val', 'ab')
.type('{enter}')
.should('have.value', 'ab')
cy.getAll('ce', 'keydown keypress keyup input textInput').each(shouldBeCalledOnce)
})
it('{enter} correct events in textarea', () => {
const input = cy.$$('textarea:first')
attachKeyListeners({ input })
cy.get('textarea:first')
.invoke('val', 'foo')
.scrollIntoView()
.type('{enter}')
.should('have.value', 'foo\n')
cy.getAll('input', 'keydown keyup keypress input textInput').each(shouldBeCalledOnce)
})
it('{enter} correct events in textarea when preventDefault', () => {
const input = cy.$$('textarea:first')
attachKeyListeners({ input })
input.on('keydown', (e) => {
if (e.key === 'Enter') {
e.stopPropagation()
e.preventDefault()
}
})
cy.get(':text:first').invoke('val', 'ab').type('{enter}').then(() => {
done()
})
cy.get('textarea:first')
.invoke('val', 'foo')
.scrollIntoView()
.type('{enter}')
.should('have.value', 'foo')
cy.getAll('input', 'keydown keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'keypress textInput input').each(shouldNotBeCalled)
})
it('does not fire input event when no text inserted', (done) => {