fix(driver): fix moveToStart/End, add tests (#8466)

Co-authored-by: KHeo <sainthkh@naver.com>
This commit is contained in:
Ben Kucera
2020-09-02 16:16:48 -04:00
committed by GitHub
parent 3fdfc3b453
commit 8d5c75634e
5 changed files with 105 additions and 78 deletions
@@ -8,6 +8,7 @@ const { getCommandLogWithText,
shouldBeCalled,
shouldBeCalledOnce,
shouldNotBeCalled,
expectCaret,
} = require('../../../support/utils')
const fail = function (str) {
@@ -800,53 +801,20 @@ describe('src/cy/commands/actions/click', () => {
it('places cursor at the end of [contenteditable]', () => {
cy.get('[contenteditable]:first')
.invoke('html', '<div><br></div>').click()
.then(($el) => {
const range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.outerHTML).to.eql('<div><br></div>')
expect(range.startOffset).to.eql(0)
expect(range.endContainer.outerHTML).to.eql('<div><br></div>')
expect(range.endOffset).to.eql(0)
})
.then(expectCaret(0))
cy.get('[contenteditable]:first')
.invoke('html', 'foo').click()
.then(($el) => {
const range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.nodeValue).to.eql('foo')
expect(range.startOffset).to.eql(3)
expect(range.endContainer.nodeValue).to.eql('foo')
expect(range.endOffset).to.eql(3)
})
.then(expectCaret(3))
cy.get('[contenteditable]:first')
.invoke('html', '<div>foo</div>').click()
.then(($el) => {
const range = $el.get(0).ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer.nodeValue).to.eql('foo')
expect(range.startOffset).to.eql(3)
expect(range.endContainer.nodeValue).to.eql('foo')
expect(range.endOffset).to.eql(3)
})
.then(expectCaret(3))
cy.get('[contenteditable]:first')
// firefox: prevent contenteditable from disappearing (dont set to empty)
// firefox headless: prevent contenteditable from disappearing (dont set to empty)
.invoke('html', '<br>').click()
.then(($el) => {
const el = $el.get(0)
const range = el.ownerDocument.getSelection().getRangeAt(0)
expect(range.startContainer).to.eql(el)
expect(range.startOffset).to.eql(0)
expect(range.endContainer).to.eql(el)
expect(range.endOffset).to.eql(0)
})
.then(expectCaret(0))
})
it('can click SVG elements', () => {
@@ -40,6 +40,40 @@ describe('src/cy/commands/actions/type - #type special chars', () => {
cy.visit('/fixtures/dom.html')
})
// https://github.com/cypress-io/cypress/issues/5502
describe('moves the cursor from in-between to the start or the end', () => {
it('input', () => {
cy.get('input:first')
.type('123{moveToStart}456{moveToEnd}789')
.should('have.value', '456123789')
})
describe('contenteditable', () => {
it('basic tests', () => {
cy.get('[contenteditable]:first')
.type('123{enter}456{enter}789{enter}abc{moveToStart}def{moveToEnd}ghi')
.then(($div) => {
expect($div.get(0).innerText.trim()).to.eql('def123\n456\n789\nabcghi')
})
})
it('bare text in front and back of an element', () => {
cy.get('[contenteditable]:first')
.invoke('html', '123<div>456</div>789')
.type('abc{moveToStart}def{moveToEnd}ghi')
.then(($div) => {
expect($div.get(0).innerText.trim()).to.eql('def123\n456\n789abcghi')
})
})
})
it('textarea', () => {
cy.get('textarea:first')
.type('123{enter}456{enter}789{enter}abc{moveToStart}def{moveToEnd}ghi')
.should('have.value', 'def123\n456\n789\nabcghi')
})
})
context('parseSpecialCharSequences: false', () => {
it('types special character sequences literally', (done) => {
cy.get(':text:first').invoke('val', 'foo')
+7
View File
@@ -105,6 +105,13 @@ export const trimInnerText = ($el) => {
return _.trimEnd($el.get(0).innerText, '\n')
}
export const expectCaret = (start, end) => {
return ($el) => {
end = end == null ? start : end
expect(Cypress.dom.getSelectionBounds($el.get(0))).to.deep.eq({ start, end })
}
}
Cypress.Commands.add('getAll', getAllFn)
const chaiSubset = require('chai-subset')
+1
View File
@@ -1366,6 +1366,7 @@ export {
getContainsSelector,
getFirstDeepestElement,
getFirstCommonAncestor,
getTagName,
getFirstParentWithTagName,
getFirstFixedOrStickyPositionParent,
getFirstStickyPositionParent,
+57 -40
View File
@@ -36,34 +36,31 @@ const _getSelectionBoundsFromInput = function (el) {
}
}
const _getSelectionRange = (doc: Document) => {
const sel = doc.getSelection()
// selection has at least one range (most always 1; only 0 at page load)
if (sel && sel.rangeCount) {
// get the first (usually only) range obj
return sel.getRangeAt(0)
}
return doc.createRange()
}
const _getSelectionBoundsFromContentEditable = function (el) {
const doc = $document.getDocumentFromElement(el)
const range = _getSelectionRange(doc)
const hostContenteditable = getHostContenteditable(range.commonAncestorContainer)
if (hostContenteditable === el) {
return {
start: range.startOffset,
end: range.endOffset,
}
}
return {
const _getSelectionBoundsFromContentEditable = (el) => {
const pos = {
start: 0,
end: 0,
}
const sel = _getSelectionByEl(el)
if (!sel.rangeCount) {
return pos
}
const range = sel.getRangeAt(0)
let preCaretRange = range.cloneRange()
preCaretRange.selectNodeContents(el)
preCaretRange.setEnd(range.endContainer, range.endOffset)
pos.end = preCaretRange.toString().length
preCaretRange.selectNodeContents(el)
preCaretRange.setEnd(range.startContainer, range.startOffset)
pos.start = preCaretRange.toString().length
return pos
}
// TODO get ACTUAL caret position in contenteditable, not line
@@ -129,21 +126,17 @@ const _hasContenteditableAttr = (el) => {
return attr !== undefined && attr !== null && attr !== 'false'
}
const getHostContenteditable = function (el) {
const getHostContenteditable = function (el: HTMLElement) {
let curEl = el
while (curEl.parentElement && !_hasContenteditableAttr(curEl)) {
curEl = curEl.parentElement
}
// if there's no host contenteditable, we must be in designmode
// if there's no host contenteditable, we must be in designMode
// so act as if the documentElement (html element) is the host contenteditable
if (!_hasContenteditableAttr(curEl)) {
if ($document.isDocument(curEl)) {
return curEl.documentElement
}
return el.ownerDocument.documentElement
return $document.getDocumentFromElement(el).documentElement
}
return curEl
@@ -563,23 +556,47 @@ const _moveSelectionTo = function (toStart: boolean, el: HTMLElement, options =
}
if ($elements.isContentEditable(el)) {
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
const selection = doc.getSelection()
const selection = _getSelectionByEl(el)
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'
if (Cypress.isBrowser({ family: 'firefox' })) {
// FireFox doesn't treat a selectAll+arrow the same as clicking the start/end of a contenteditable
// so we need to select the specific nodes inside the contenteditable.
const root = getHostContenteditable(el)
selection.modify('move', direction, 'line')
let elToSelect = root.childNodes[toStart ? 0 : root.childNodes.length - 1]
return
// in firefox, when an empty contenteditable is a single <br> element or <div><br/></div>
// its innerText will be '\n' (maybe find a more efficient measure)
if (!elToSelect || root.innerText === '\n') {
// we must be in an empty contenteditable, so we're already at both the start and end
return
}
// if we're on a <br> but the text isn't empty, we need to
if ($elements.getTagName(elToSelect) === 'br') {
if (root.childNodes.length < 2) {
// no other node to target, shouldn't really happen but we should behave like the contenteditable is empty
return
}
elToSelect = toStart ? root.childNodes[1] : root.childNodes[root.childNodes.length - 2]
}
const range = selection.getRangeAt(0)
range.selectNodeContents(elToSelect)
} else {
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
}
toStart ? selection.collapseToStart() : selection.collapseToEnd()
}
return false
return
}
const moveSelectionToEnd = _.curry(_moveSelectionTo)(false)