mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-24 07:59:12 -05:00
fix(driver): fix moveToStart/End, add tests (#8466)
Co-authored-by: KHeo <sainthkh@naver.com>
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1366,6 +1366,7 @@ export {
|
||||
getContainsSelector,
|
||||
getFirstDeepestElement,
|
||||
getFirstCommonAncestor,
|
||||
getTagName,
|
||||
getFirstParentWithTagName,
|
||||
getFirstFixedOrStickyPositionParent,
|
||||
getFirstStickyPositionParent,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user