Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: cy.type in hidden inputs, contenteditable selections #5865

Merged
merged 10 commits into from Dec 4, 2019
12 changes: 3 additions & 9 deletions packages/driver/src/cy/keyboard.ts
Expand Up @@ -528,27 +528,21 @@ const keyboardMappings: { [key: string]: KeyDetailsPartial } = {
selectAll: {
key: 'selectAll',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)

return $selection.selectAll(doc)
$selection.selectAll(el)
},
simulatedDefaultOnly: true,
},
moveToStart: {
key: 'moveToStart',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)

return $selection.moveSelectionToStart(doc)
$selection.moveSelectionToStart(el)
},
simulatedDefaultOnly: true,
},
moveToEnd: {
key: 'moveToEnd',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)

return $selection.moveSelectionToEnd(doc)
$selection.moveSelectionToEnd(el)
},
simulatedDefaultOnly: true,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/driver/src/cy/mouse.js
Expand Up @@ -380,7 +380,7 @@ const create = (state, keyboard, focused) => {

if (shouldMoveCursorToEndAfterMousedown($elToFocus[0])) {
debug('moveSelectionToEnd due to click')
$selection.moveSelectionToEnd($dom.getDocumentFromElement($elToFocus[0]), { onlyIfEmptySelection: true })
$selection.moveSelectionToEnd($elToFocus[0], { onlyIfEmptySelection: true })
}

return mouseDownPhase
Expand Down
39 changes: 27 additions & 12 deletions packages/driver/src/dom/selection.ts
Expand Up @@ -174,6 +174,13 @@ const setSelectionRange = function (el, 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()
}

/**
* @returns {boolean} whether or not input events are needed
*/
Expand All @@ -199,7 +206,15 @@ const deleteRightOfCursor = function (el) {
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)

$elements.callNativeMethod(selection, 'modify', 'extend', 'forward', 'character')
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'forward',
'character',
)
}

if ($elements.getNativeProp(selection, 'isCollapsed')) {
// there's nothing to delete
Expand Down Expand Up @@ -243,12 +258,14 @@ const deleteLeftOfCursor = function (el) {
// there is no 'backwardDelete' command for execCommand, so use the Selection API
const selection = _getSelectionByEl(el)

$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
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'backward',
'character'
)
}

deleteSelectionContents(el)
Expand Down Expand Up @@ -401,9 +418,7 @@ const isCollapsed = function (el) {
return false
}

const selectAll = function (doc) {
const el = doc.activeElement

const selectAll = function (el: HTMLElement) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
setSelectionRange(el, 0, $elements.getNativeProp(el, 'value').length)

Expand Down Expand Up @@ -445,12 +460,12 @@ const getSelectionBounds = function (el) {
}
}

const _moveSelectionTo = function (toStart: boolean, doc: Document, options = {}) {
const _moveSelectionTo = function (toStart: boolean, el: HTMLElement, options = {}) {
const opts = _.defaults({}, options, {
onlyIfEmptySelection: false,
})

const el = $elements.getActiveElByDocument(doc)
const doc = $document.getDocumentFromElement(el)

if ($elements.isInput(el) || $elements.isTextarea(el)) {
if (opts.onlyIfEmptySelection) {
Expand Down
139 changes: 124 additions & 15 deletions packages/driver/test/cypress/integration/commands/actions/type_spec.js
Expand Up @@ -1989,6 +1989,20 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('input', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in input with selection', () => {
const input = cy.$$(':text:first')

attachKeyListeners({ input })

cy.get(':text:first').invoke('val', 'ab')
.focus()
.type('{selectall}{backspace}')
.should('have.value', '')

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')

Expand Down Expand Up @@ -2018,6 +2032,20 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('textarea', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in textarea with selection', () => {
const textarea = cy.$$('textarea:first')

attachKeyListeners({ textarea })

cy.get('textarea:first').invoke('val', 'ab')
.focus()
.type('{selectall}{backspace}')
.should('have.value', '')

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')

Expand Down Expand Up @@ -2047,6 +2075,24 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('ce', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in contenteditable with selection', () => {
const ce = cy.$$('[contenteditable]:first')

attachKeyListeners({ ce })

cy.get('[contenteditable]:first').invoke('text', 'ab')
.scrollIntoView()
.type('{moveToEnd}')
.then(($el) => {
$el[0].ownerDocument.getSelection().modify('extend', 'backward', 'character')
})
.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')

Expand Down Expand Up @@ -2111,6 +2157,22 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('input', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in input with selection', () => {
const input = cy.$$(':text:first')

attachKeyListeners({ input })

cy.get(':text:first').invoke('val', 'ab')

.then(($input) => $input[0].setSelectionRange(0, 0))
.focus()
.type('{selectall}{del}')
.should('have.value', '')

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')

Expand Down Expand Up @@ -2140,6 +2202,21 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('textarea', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in textarea with selection', () => {
const textarea = cy.$$('textarea:first')

attachKeyListeners({ textarea })

cy.get('textarea:first').invoke('val', 'ab')
.then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.focus()
.type('{selectall}{del}')
.should('have.value', '')

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')

Expand All @@ -2157,14 +2234,6 @@ describe('src/cy/commands/actions/type', () => {
it('correct events in contenteditable', () => {
const ce = cy.$$('[contenteditable]:first')

const keydown = cy.stub().callsFake((e) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was redundant, tested above already

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')
Expand All @@ -2176,16 +2245,25 @@ describe('src/cy/commands/actions/type', () => {
cy.getAll('ce', 'keypress textInput').each(shouldNotBeCalled)
})

it('correct events in contenteditable when noop', () => {
it('correct events in contenteditable with selection', () => {
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')
attachKeyListeners({ ce })

cy.get('[contenteditable]:first').invoke('text', 'ab')
.type('{moveToStart}')
.then(($el) => {
$el[0].ownerDocument.getSelection().modify('extend', 'forward', 'character')
})
.type('{del}')
.should('have.text', 'b')

ce.on('keydown', keydown)
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 })

Expand Down Expand Up @@ -5215,7 +5293,7 @@ https://on.cypress.io/type`)
})
})

it('can forcibly click even when being covered by another element', () => {
it('can force clear even when being covered by another element', () => {
const $input = $('<input />')
.attr('id', 'input-covered-in-span')
.prependTo(cy.$$('body'))
Expand All @@ -5240,6 +5318,37 @@ https://on.cypress.io/type`)
})
})

// https://github.com/cypress-io/cypress/issues/5835
it('can force clear when hidden in input', () => {
const input = cy.$$('input:first')
.val('foo')
.hide()

attachKeyListeners({ input })
cy.get('input:first')
.focus()
.clear({ force: true })
.should('have.value', '')

cy.getAll('input', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('input', 'textInput keypress').each(shouldNotBeCalled)
})

it('can force clear when hidden in textarea', () => {
const textarea = cy.$$('textarea:first')
.val('foo')
.hide()

attachKeyListeners({ textarea })
cy.get('textarea:first')
.focus()
.clear({ force: true })
.should('have.value', '')

cy.getAll('textarea', 'keydown input keyup').each(shouldBeCalledOnce)
cy.getAll('textarea', 'textInput keypress').each(shouldNotBeCalled)
})

it('passes timeout and interval down to click', (done) => {
const input = $('<input />').attr('id', 'input-covered-in-span').prependTo(cy.$$('body'))

Expand Down