Skip to content

Commit

Permalink
fix: iframe type error with body[contenteditable] (#6571)
Browse files Browse the repository at this point in the history
Co-authored-by: Jennifer Shehane <[email protected]>
  • Loading branch information
kuceb and jennifer-shehane authored Feb 27, 2020
1 parent cb0f32b commit 8d7f1a0
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 32 deletions.
2 changes: 2 additions & 0 deletions packages/driver/src/cy/actionability.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ const verify = function (cy, $el, options, callbacks) {
// scroll the element into view
$el.get(0).scrollIntoView()

debug('scrollIntoView:', $el[0])

if (onScroll) {
onScroll($el, 'element')
}
Expand Down
5 changes: 4 additions & 1 deletion packages/driver/src/cy/commands/actions/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,12 @@ module.exports = function (Commands, Cypress, cy, state, config) {

const handleFocused = function () {
// if it's the body, don't need to worry about focus
const isBody = options.$el.is('body')
// (unless it can be modified i.e we're in designMode or contenteditable)
const isBody = options.$el.is('body') && !$elements.isContentEditable(options.$el[0])

if (isBody) {
debug('typing into body')

return type()
}

Expand Down
4 changes: 2 additions & 2 deletions packages/driver/src/cy/focused.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ create = (state) ->
el.dispatchEvent(focusoutEvt)

fireFocus = (el) ->
## body will never emit focus events
## body will never emit focus events (unless it's contenteditable)
## so we avoid simulating this
if $elements.isBody(el)
if $elements.isBody(el) && !$elements.isContentEditable(el)
return

## if we are focusing a different element
Expand Down
21 changes: 15 additions & 6 deletions packages/driver/src/cy/mouse.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,22 +152,29 @@ const create = (state, keyboard, focused, Cypress) => {
}

const shouldMoveCursorToEndAfterMousedown = (el) => {
const _debug = debug.extend(':shouldMoveCursorToEnd')

_debug('shouldMoveToEnd?', el)
if (!$elements.isElement(el)) {
_debug('false: not element')

return false
}

if (!($elements.isInput(el) || $elements.isTextarea(el) || $elements.isContentEditable(el))) {
return false
}
_debug('false: not input/textarea/contentedtable')

if (!$elements.isFocused(el)) {
return false
}

if ($elements.isNeedSingleValueChangeInputElement(el)) {
_debug('false: is single value change input')

return false
}

_debug('true: should move to end')

return true
}

Expand Down Expand Up @@ -458,7 +465,9 @@ const create = (state, keyboard, focused, Cypress) => {
//# retrieve the first focusable $el in our parent chain
const $elToFocus = $elements.getFirstFocusableEl($(el))

debug('elToFocus:', $elToFocus[0])
if (focused.needsFocus($elToFocus, $previouslyFocused)) {
debug('el needs focus')
if ($dom.isWindow($elToFocus)) {
// if the first focusable element from the click
// is the window, then we can skip the focus event
Expand All @@ -474,9 +483,9 @@ const create = (state, keyboard, focused, Cypress) => {
}
}

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

return mouseDownPhase
Expand Down
38 changes: 34 additions & 4 deletions packages/driver/src/dom/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ const getTagName = (el) => {
// should be true for elements:
// - with [contenteditable]
// - with document.designMode = 'on'
const isContentEditable = (el: any): el is HTMLContentEditableElement => {
const isContentEditable = (el: HTMLElement): el is HTMLContentEditableElement => {
return getNativeProp(el, 'isContentEditable') || $document.getDocumentFromElement(el).designMode === 'on'
}

Expand Down Expand Up @@ -401,9 +401,9 @@ const isSvg = function (el): el is SVGElement {
}

// active element is the default if its null
// or its equal to document.body
const activeElementIsDefault = (activeElement, body) => {
return !activeElement || activeElement === body
// or it's equal to document.body that is not contenteditable
const activeElementIsDefault = (activeElement, body: HTMLElement) => {
return !activeElement || (activeElement === body && !isContentEditable(body))
}

const isFocused = (el) => {
Expand All @@ -423,19 +423,49 @@ const isFocused = (el) => {
}

const isFocusedOrInFocused = (el: HTMLElement) => {
debug('isFocusedOrInFocus', el)

const doc = $document.getDocumentFromElement(el)

if (!doc.hasFocus()) {
return false
}

const { activeElement } = doc

let elToCheckCurrentlyFocused

let isContentEditableEl = false

if (isFocusable($(el))) {
elToCheckCurrentlyFocused = el
} else if (isContentEditable(el)) {
isContentEditableEl = true
elToCheckCurrentlyFocused = $selection.getHostContenteditable(el)
}

debug('elToCheckCurrentlyFocused', elToCheckCurrentlyFocused)

if (elToCheckCurrentlyFocused && elToCheckCurrentlyFocused === activeElement) {
if (isContentEditableEl) {
// we make sure the the current document selection (blinking cursor) is inside the element
const sel = doc.getSelection()

if (sel?.rangeCount) {
const range = sel.getRangeAt(0)
const curSelectionContainer = range.commonAncestorContainer

const selectionInsideElement = el.contains(curSelectionContainer)

debug('isInFocused by document selection?', selectionInsideElement, ':', curSelectionContainer, 'is inside', el)

return selectionInsideElement
}

// no selection, not in focused
return false
}

return true
}

Expand Down
8 changes: 6 additions & 2 deletions packages/driver/src/dom/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@ const getHostContenteditable = function (el) {
}

// if there's no host contenteditable, we must be in designmode
// so act as if the body element is the host contenteditable
// so act as if the documentElement (html element) is the host contenteditable
if (!_hasContenteditableAttr(curEl)) {
return el.ownerDocument.body
if ($document.isDocument(curEl)) {
return curEl.documentElement
}

return el.ownerDocument.documentElement
}

return curEl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ const {
shouldBeCalledOnce,
} = require('../../../support/utils')

const expectTextEndsWith = (expected) => {
return ($el) => {
const text = $el.text().trim()

const passed = text.endsWith(expected)

const displayText = text.length > 300 ? (`${text.slice(0, 100)}...${text.slice(-100)}`) : text

assert(passed, `expected ${displayText} to end with ${expected}`)
}
}

describe('src/cy/commands/actions/type - #type', () => {
beforeEach(() => {
cy.visit('/fixtures/dom.html')
Expand Down Expand Up @@ -1436,13 +1448,13 @@ describe('src/cy/commands/actions/type - #type', () => {

it('<area> element', () => {
cy.$$(`
<map name="map">
<area shape="circle" coords="0,0,100"
href="#"
target="_blank" alt="area" />
</map>
<img usemap="#map" src="/__cypress/static/favicon.ico" alt="image" />
`).prependTo(cy.$$('body'))
<map name="map">
<area shape="circle" coords="0,0,100"
href="#"
target="_blank" alt="area" />
</map>
<img usemap="#map" src="/__cypress/static/favicon.ico" alt="image" />
`).prependTo(cy.$$('body'))

let keydown = cy.stub()

Expand Down Expand Up @@ -1698,8 +1710,7 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})

it(`can type into an iframe with designmode = 'on'`, () => {
// append a new iframe to the body
function insertIframe () {
cy.$$('<iframe id="generic-iframe" src="/fixtures/generic.html" style="height: 500px"></iframe>')
.appendTo(cy.$$('body'))

Expand All @@ -1715,7 +1726,47 @@ describe('src/cy/commands/actions/type - #type', () => {
.should(() => {
expect(loaded).to.eq(true)
})
}

it('can type in designmode="on"', () => {
cy.timeout(100)
cy.state('document').designMode = 'on'
cy.state('document').documentElement.focus()
cy.get('div.item:first')
.type('111')

cy.get('body').then(expectTextEndsWith('111'))
})

// TODO[breaking]: we should edit div.item:first text content instead of
// moving to the end of the host contenteditable. This will allow targeting
// specific elements to simplify testing rich editors
it('can type in body[contenteditable]', () => {
cy.state('document').body.setAttribute('contenteditable', true)
cy.state('document').documentElement.focus()
cy.get('div.item:first')
.type('111')

cy.get('body')
.then(expectTextEndsWith('111'))
})

// https://github.com/cypress-io/cypress/issues/5930
it('can type into an iframe with body[contenteditable]', () => {
insertIframe()
cy.get('#generic-iframe').then(($iframe) => {
cy.wrap($iframe.contents().find('html').first().find('body'))
.then(($body) => {
$body.attr('contenteditable', true)
})
.type('111')
.then(expectTextEndsWith('111'))
})
})

it(`can type into an iframe with designmode = 'on'`, () => {
// append a new iframe to the body
insertIframe()
// type text into iframe
cy.get('#generic-iframe')
.then(($iframe) => {
Expand Down Expand Up @@ -2501,11 +2552,11 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})

it('accurately returns document body el with no falsey contenteditable="false" attr', () => {
it('accurately returns documentElement el when contenteditable="false" attr', () => {
cy.$$('<div contenteditable="false"><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))

cy.get('#ce-inner1').then(($el) => {
expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0].ownerDocument.body)
expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0].ownerDocument.documentElement)
})
})

Expand Down Expand Up @@ -2650,8 +2701,8 @@ describe('src/cy/commands/actions/type - #type', () => {
const el = $el.get(0)

el.innerHTML = 'start' +
'<div>middle</div>' +
'<div>end</div>'
'<div>middle</div>' +
'<div>end</div>'

cy.get('[contenteditable]:first')
// move cursor to beginning of div
Expand All @@ -2666,8 +2717,8 @@ describe('src/cy/commands/actions/type - #type', () => {
const el = $el.get(0)

el.innerHTML = 'start' +
'<div>middle</div>' +
'<div>end</div>'
'<div>middle</div>' +
'<div>end</div>'

cy.get('[contenteditable]:first').type(`${'{leftarrow}'.repeat(12)}[_I_]`).then(($el) => {
expect(trimInnerText($el)).to.eql('star[_I_]t\nmiddle\nend')
Expand Down Expand Up @@ -2937,7 +2988,7 @@ describe('src/cy/commands/actions/type - #type', () => {
const table = this.lastLog.invoke('consoleProps').table[2]()

// eslint-disable-next-line
console.table(table.data, table.columns)
console.table(table.data, table.columns)

expect(table.name).to.eq('Keyboard Events')
const expectedTable = {
Expand Down Expand Up @@ -2981,7 +3032,7 @@ describe('src/cy/commands/actions/type - #type', () => {
const table = this.lastLog.invoke('consoleProps').table[2]()

// eslint-disable-next-line
console.table(table.data, table.columns)
console.table(table.data, table.columns)

expect(table.data).to.deep.eq({
1: { Typed: 'f', 'Events Fired': 'keydown, keyup', 'Active Modifiers': null, Details: '{ code: KeyF, which: 70 }', 'Prevented Default': true, 'Target Element': $el[0] },
Expand Down

4 comments on commit 8d7f1a0

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8d7f1a0 Feb 27, 2020

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/linux-x64/circle-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-267362/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/circle-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-267342/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8d7f1a0 Feb 27, 2020

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/darwin-x64/circle-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-267409/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/circle-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-267348/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8d7f1a0 Feb 27, 2020

Choose a reason for hiding this comment

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

AppVeyor has built the win32 x64 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

Instructions are included below, depending on the shell you are using.

In Command Prompt (cmd.exe):

set CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-x64/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

In PowerShell:

$env:CYPRESS_INSTALL_BINARY = https://cdn.cypress.io/beta/binary/4.0.3/win32-x64/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

In Git Bash:

export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-x64/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

Using cross-env:

If the above commands do not work for you, you can also try using cross-env:

npm i -g cross-env
cross-env CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-x64/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8d7f1a0 Feb 27, 2020

Choose a reason for hiding this comment

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

AppVeyor has built the win32 ia32 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

Instructions are included below, depending on the shell you are using.

In Command Prompt (cmd.exe):

set CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-ia32/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

In PowerShell:

$env:CYPRESS_INSTALL_BINARY = https://cdn.cypress.io/beta/binary/4.0.3/win32-ia32/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

In Git Bash:

export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-ia32/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip
npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

Using cross-env:

If the above commands do not work for you, you can also try using cross-env:

npm i -g cross-env
cross-env CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/4.0.3/win32-ia32/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.zip npm install https://cdn.cypress.io/beta/npm/4.0.3/appveyor-develop-8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683-31112188/cypress.tgz

Please sign in to comment.