mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-13 18:50:18 -06:00
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:
committed by
Jennifer Shehane
parent
02515fec61
commit
74006acf66
@@ -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)
|
||||
|
||||
|
||||
@@ -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(@)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user