diff --git a/packages/app/src/runner/logger.ts b/packages/app/src/runner/logger.ts
index 2542f89f04bc..82c04f943fcd 100644
--- a/packages/app/src/runner/logger.ts
+++ b/packages/app/src/runner/logger.ts
@@ -32,7 +32,7 @@ export const logger = {
this._logValues(consoleProps)
this._logArgs(consoleProps)
this._logGroups(consoleProps)
- this._logTable(consoleProps)
+ this._logTables(consoleProps)
},
_logValues (consoleProps: any) {
@@ -118,43 +118,46 @@ export const logger = {
})
},
- _logTable (consoleProps: any) {
- if (isMultiEntryTable(consoleProps.table)) {
- _.each(
- _.sortBy(consoleProps.table, (val, key) => key),
- (table) => {
- return this._logTable({ table })
- },
- )
+ _logTables (consoleProps: any) {
+ const logTable = ({ name, data, columns }) => {
+ let tableData = data
- return
- }
+ if (Cypress.isBrowser('webkit')) {
+ // WebKit will hang when we attempt to log element references
+ // within a table. We replace the element with a simplified display
+ // string in this case.
+ // https://bugs.webkit.org/show_bug.cgi?id=244100
- const table = this._getTable(consoleProps)
+ const getSimplifiedElementDisplay = (element: Element) => {
+ let display = element.tagName.toLowerCase()
- if (!table) return
+ if (element.id) {
+ display += `#${element.id}`
+ }
- if (_.isArray(table)) {
- console.table(table)
- } else {
- console.group(table.name)
- console.table(table.data, table.columns)
- console.groupEnd()
- }
- },
+ element.classList.forEach((className) => {
+ display += `.${className}`
+ })
- _getTable (consoleProps: any): Table | Table[] | undefined {
- const table = _.result
(consoleProps, 'table')
+ return display
+ }
- if (!table) return
+ tableData = data.map((rowObj) => {
+ return Object.entries(rowObj).reduce((acc: any, value) => {
+ acc[value[0]] = _.isElement(value[1]) ? getSimplifiedElementDisplay(value[1] as Element) : value[1]
- return table
- },
-}
+ return acc
+ }, {})
+ })
+ }
+
+ console.group(name)
+ console.table(tableData, columns)
+ console.groupEnd()
+ }
-const isMultiEntryTable = (table: Table) => {
- return !_.isFunction(table) &&
- !_.some(_.keys(table)
- .map((x) => isNaN(parseInt(x, 10)))
- .filter(Boolean), true)
+ _.each(_.sortBy(consoleProps.table, (val, key) => key), (getTableData: () => Table) => {
+ return logTable(getTableData())
+ })
+ },
}
diff --git a/packages/driver/cypress/e2e/commands/actions/click.cy.js b/packages/driver/cypress/e2e/commands/actions/click.cy.js
index 1ec6075c6269..3fa9e8bf358f 100644
--- a/packages/driver/cypress/e2e/commands/actions/click.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/click.cy.js
@@ -45,9 +45,9 @@ const getMidPoint = (el) => {
}
const isFirefox = Cypress.isBrowser('firefox')
+const isWebKit = Cypress.isBrowser('webkit')
-// TODO(webkit): fix+unskip for experimental webkit
-describe('src/cy/commands/actions/click', { browser: '!webkit' }, () => {
+describe('src/cy/commands/actions/click', () => {
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
@@ -3596,7 +3596,8 @@ describe('src/cy/commands/actions/click', { browser: '!webkit' }, () => {
cy.getAll('el', 'mousedown contextmenu mouseup').each(shouldNotBeCalled)
- cy.getAll('el', 'pointerdown pointerup').each(isFirefox ? shouldNotBeCalled : shouldBeCalled)
+ // On disabled inputs, pointer events are still fired in chrome, not in firefox or webkit
+ cy.getAll('el', 'pointerdown pointerup').each(isFirefox || isWebKit ? shouldNotBeCalled : shouldBeCalled)
})
it('rightclick cancel contextmenu', () => {
@@ -3929,8 +3930,7 @@ describe('src/cy/commands/actions/click', { browser: '!webkit' }, () => {
})
})
-// TODO(webkit): fix+unskip for experimental webkit
-describe('shadow dom', { browser: '!webkit' }, () => {
+describe('shadow dom', () => {
beforeEach(() => {
cy.visit('/fixtures/shadow-dom.html')
})
@@ -3993,8 +3993,7 @@ describe('shadow dom', { browser: '!webkit' }, () => {
})
})
-// TODO(webkit): fix+unskip for experimental webkit
-describe('mouse state', { browser: '!webkit' }, () => {
+describe('mouse state', () => {
describe('mouse/pointer events', () => {
beforeEach(() => {
cy.visit('http://localhost:3500/fixtures/dom.html')
@@ -4064,12 +4063,28 @@ describe('mouse state', { browser: '!webkit' }, () => {
y: 10,
}
+ const coordsWebKit = {
+ clientX: 500,
+ clientY: 10,
+ layerX: 500,
+ layerY: 226,
+ pageX: 500,
+ pageY: 226,
+ screenX: 500,
+ screenY: 10,
+ x: 500,
+ y: 10,
+ }
+
let coords
switch (Cypress.browser.family) {
case 'firefox':
coords = coordsFirefox
break
+ case 'webkit':
+ coords = coordsWebKit
+ break
default:
coords = coordsChrome
break
@@ -4498,9 +4513,10 @@ describe('mouse state', { browser: '!webkit' }, () => {
// cy.wrap(onAction).should('calledOnce')
cy.getAll('btn', 'pointerover pointerenter').each(shouldBeCalledOnce)
- cy.getAll('btn', 'pointerdown pointerup').each(isFirefox ? shouldNotBeCalled : shouldBeCalledOnce)
- cy.getAll('btn', 'mouseover mouseenter').each(isFirefox ? shouldBeCalled : shouldNotBeCalled)
+ // On disabled inputs, pointer events are still fired in chrome, not in firefox or webkit
+ cy.getAll('btn', 'pointerdown pointerup').each(isFirefox || isWebKit ? shouldNotBeCalled : shouldBeCalledOnce)
+ cy.getAll('btn', 'mouseover mouseenter').each(isFirefox || isWebKit ? shouldBeCalled : shouldNotBeCalled)
cy.getAll('btn', 'mousedown mouseup click').each(shouldNotBeCalled)
})
@@ -4524,7 +4540,9 @@ describe('mouse state', { browser: '!webkit' }, () => {
cy.get('#btn').click()
cy.getAll('btn', 'pointerdown mousedown').each(shouldBeCalledOnce)
- cy.getAll('btn', 'pointerup').each(isFirefox ? shouldNotBeCalled : shouldBeCalledOnce)
+
+ // On disabled inputs, pointer events are still fired in chrome, not in firefox or webkit
+ cy.getAll('btn', 'pointerup').each(isFirefox || isWebKit ? shouldNotBeCalled : shouldBeCalledOnce)
cy.getAll('btn', 'mouseup click').each(shouldNotBeCalled)
})
@@ -4599,8 +4617,8 @@ describe('mouse state', { browser: '!webkit' }, () => {
cy.getAll('btn', 'mousedown mouseup click').each(shouldNotBeCalled)
- // on disabled inputs, pointer events are still fired in chrome, not in firefox
- cy.getAll('btn', 'pointerdown pointerup').each(isFirefox ? shouldNotBeCalled : shouldBeCalled)
+ // On disabled inputs, pointer events are still fired in chrome, not in firefox or webkit
+ cy.getAll('btn', 'pointerdown pointerup').each(isFirefox || isWebKit ? shouldNotBeCalled : shouldBeCalled)
})
it('can target new element after mousedown sequence', () => {
diff --git a/packages/driver/cypress/e2e/commands/actions/type.cy.js b/packages/driver/cypress/e2e/commands/actions/type.cy.js
index f958043dddd9..301f660b098e 100644
--- a/packages/driver/cypress/e2e/commands/actions/type.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/type.cy.js
@@ -22,8 +22,9 @@ const expectTextEndsWith = (expected) => {
}
}
-// TODO(webkit): fix+unskip for experimental webkit
-describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
+const isWebKit = Cypress.isBrowser('webkit')
+
+describe('src/cy/commands/actions/type - #type', () => {
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
@@ -957,6 +958,25 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
.should('have.value', 'baroo')
})
+ // WebKit will select all input content on focus. This causes our
+ // cursor placement logic to be ignored, as we interpret the default
+ // selection as a user-provided selection that we do not want to override.
+ // We work around this by preventing the default selection on focus using
+ // our own capture-phase 'focus' event handler; this test ensures that user-set
+ // capture-phase events continue to function as expected for the purpose
+ // of selection updates.
+ it('respects changed selection in focus handler during capture phase', () => {
+ cy.get('#input-without-value')
+ .then(($el) => {
+ $el.val('foo')
+ $el.get(0).addEventListener('focus', (e) => {
+ e.currentTarget.setSelectionRange(0, 1)
+ }, { capture: true })
+ })
+ .type('bar')
+ .should('have.value', 'baroo')
+ })
+
it('overwrites text when selectAll in mouseup handler', () => {
cy.$$('#input-without-value').val('0').mouseup(function () {
$(this).select()
@@ -1197,7 +1217,7 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
})
})
- it('inserts text after existing text ', () => {
+ it('inserts text after existing text', () => {
cy.get('#number-with-value').type('34').then(($text) => {
expect($text).to.have.value('1234')
})
@@ -1794,33 +1814,36 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
})
// https://github.com/cypress-io/cypress/issues/7088
+ // In WebKit, setting the inputType is not supported by the InputEvent constructor.
+ // This results in the inputType being unset in any of the simulated beforeinput events.
+ // https://bugs.webkit.org/show_bug.cgi?id=170416
describe('beforeInput event', () => {
it('sends beforeinput in text input', () => {
const call1 = (e) => {
expect(e.code).not.exist
expect(e.data).eq(' ')
- expect(e.inputType).eq('insertText')
+ !isWebKit && expect(e.inputType).eq('insertText')
stub.callsFake(call2)
}
const call2 = (e) => {
expect(e.code).not.exist
expect(e.data).eq('f')
- expect(e.inputType).eq('insertText')
+ !isWebKit && expect(e.inputType).eq('insertText')
stub.callsFake(call3)
}
const call3 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('insertLineBreak')
+ !isWebKit && expect(e.inputType).eq('insertLineBreak')
stub.callsFake(call4)
}
const call4 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('deleteContentBackward')
+ !isWebKit && expect(e.inputType).eq('deleteContentBackward')
stub.callsFake(call5)
}
const call5 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('deleteContentForward')
+ !isWebKit && expect(e.inputType).eq('deleteContentForward')
}
const stub = cy.stub()
@@ -1843,28 +1866,28 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
const call1 = (e) => {
expect(e.code).not.exist
expect(e.data).eq(' ')
- expect(e.inputType).eq('insertText')
+ !isWebKit && expect(e.inputType).eq('insertText')
stub.callsFake(call2)
}
const call2 = (e) => {
expect(e.code).not.exist
expect(e.data).eq('f')
- expect(e.inputType).eq('insertText')
+ !isWebKit && expect(e.inputType).eq('insertText')
stub.callsFake(call3)
}
const call3 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('insertLineBreak')
+ !isWebKit && expect(e.inputType).eq('insertLineBreak')
stub.callsFake(call4)
}
const call4 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('deleteContentBackward')
+ !isWebKit && expect(e.inputType).eq('deleteContentBackward')
stub.callsFake(call5)
}
const call5 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('deleteContentForward')
+ !isWebKit && expect(e.inputType).eq('deleteContentForward')
}
const stub = cy.stub()
@@ -1883,6 +1906,10 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
})
})
+ // In WebKit, simulated `beforeinput` events are not emitted for
+ // contenteditable inputs. The stubs are receiving the
+ // browser-emitted events, which is why the inputType values
+ // of these events are populated.
it('sends beforeinput in [contenteditable]', () => {
const call1 = (e) => {
expect(e.code).not.exist
@@ -1908,7 +1935,14 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
}
const call5 = (e) => {
expect(e.data).eq(null)
- expect(e.inputType).eq('deleteContentForward')
+
+ if (isWebKit) {
+ // WebKit does not distinguish between forward/backward
+ // deletion within a contenteditable field
+ expect(e.inputType).eq('deleteContentBackward')
+ } else {
+ expect(e.inputType).eq('deleteContentForward')
+ }
}
const stub = cy.stub()
@@ -1927,7 +1961,7 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
})
})
- it('beforeinput special inputTypes', () => {
+ it('beforeinput special inputTypes in [contenteditable] (not WebKit)', { browser: '!webkit' }, () => {
const call1 = (e) => {
expect(e.code).not.exist
expect(e.data).eq(null)
@@ -1967,6 +2001,50 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
})
})
+ // We do not emit simulated `beforeinput` events for the WebKit browser's
+ // contenteditable fields, as `execCommand('insertText')` will emit `beforeinput`
+ // when called. As a result, we do not emit as many events as other browsers in
+ // this test case, which includes no-op deletions due to a terminal cursor position.
+ it('beforeinput special inputTypes in [contenteditable] (WebKit)', { browser: 'webkit' }, () => {
+ const call1 = (e) => {
+ expect(e.code).not.exist
+ expect(e.data).eq(null)
+
+ // WebKit does not distinguish between forward/backward
+ // deletion within a contenteditable field using `execCommand('delete')`
+ expect(e.inputType).eq('deleteContentBackward')
+
+ stub.callsFake(call2)
+ }
+ const call2 = (e) => {
+ expect(e.code).not.exist
+ expect(e.data).eq(null)
+
+ // WebKit does not distinguish between forward/backward
+ // deletion within a contenteditable field using `execCommand('delete')`
+ expect(e.inputType).eq('deleteContentBackward')
+ }
+
+ const stub = cy.stub()
+ .callsFake(call1)
+
+ cy.get('#input-types [contenteditable]')
+ .then(($el) => {
+ $el.text('foo bar baz')
+ $el[0].addEventListener('beforeinput', stub)
+ })
+ // This command does not result in a change, as the cursor is in the right-most position
+ // and there is nothing to delete. This causes WebKit to not emit a beforeinput event.
+ .type('{ctrl}{del}')
+ // This command also does not result in a change, for the same reason.
+ .type('{ctrl}{shift}{del}')
+ .type('{ctrl}{backspace}')
+ .type('{ctrl}{shift}{backspace}')
+ .then(($el) => {
+ expect(stub).callCount(2)
+ })
+ })
+
it('can cancel beforeinput', () => {
let callCount = 0
@@ -2356,19 +2434,16 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
.then(function ($input) {
const table = this.lastLog.invoke('consoleProps').table[2]()
- // eslint-disable-next-line
- console.table(table.data, table.columns)
-
expect(table.name).to.eq('Keyboard Events')
- const expectedTable = {
- 1: { 'Details': '{ code: KeyH, which: 72 }', Typed: 'h', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
- 2: { 'Details': '{ code: ControlLeft, which: 17 }', Typed: '{ctrl}', 'Events Fired': 'keydown', 'Active Modifiers': 'ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
- 3: { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{alt}', 'Events Fired': 'keydown', 'Active Modifiers': 'alt, ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
- 4: { 'Details': '{ code: Equal, which: 187 }', Typed: '+', 'Events Fired': 'keydown, keyup', 'Active Modifiers': 'alt, ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
- 5: { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{alt}', 'Events Fired': 'keyup', 'Active Modifiers': 'ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
- 6: { 'Details': '{ code: ControlLeft, which: 17 }', Typed: '{ctrl}', 'Events Fired': 'keyup', 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
- 7: { 'Details': '{ code: KeyI, which: 73 }', Typed: 'i', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
- }
+ const expectedTable = [
+ { 'Details': '{ code: KeyH, which: 72 }', Typed: 'h', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: ControlLeft, which: 17 }', Typed: '{ctrl}', 'Events Fired': 'keydown', 'Active Modifiers': 'ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{alt}', 'Events Fired': 'keydown', 'Active Modifiers': 'alt, ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: Equal, which: 187 }', Typed: '+', 'Events Fired': 'keydown, keyup', 'Active Modifiers': 'alt, ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{alt}', 'Events Fired': 'keyup', 'Active Modifiers': 'ctrl', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: ControlLeft, which: 17 }', Typed: '{ctrl}', 'Events Fired': 'keyup', 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: KeyI, which: 73 }', Typed: 'i', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
+ ]
// uncomment for debugging
// _.each(table.data, (v, i) => expect(v).containSubset(expectedTable[i]))
@@ -2934,27 +3009,23 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
.then(function ($input) {
const table = this.lastLog.invoke('consoleProps').table[2]()
- // eslint-disable-next-line
- console.table(table.data, table.columns)
-
expect(table.name).to.eq('Keyboard Events')
- const expectedTable = {
- 1: { 'Details': '{ code: MetaLeft, which: 91 }', Typed: '{cmd}', 'Events Fired': 'keydown', 'Active Modifiers': 'meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 2: { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{option}', 'Events Fired': 'keydown', 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 3: { 'Details': '{ code: KeyF, which: 70 }', Typed: 'f', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 4: { 'Details': '{ code: KeyO, which: 79 }', Typed: 'o', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 5: { 'Details': '{ code: KeyO, which: 79 }', Typed: 'o', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 6: { 'Details': '{ code: Enter, which: 13 }', Typed: '{enter}', 'Events Fired': `keydown, keypress, beforeinput, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 7: { 'Details': '{ code: KeyB, which: 66 }', Typed: 'b', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 8: { 'Details': '{ code: ArrowLeft, which: 37 }', Typed: '{leftarrow}', 'Events Fired': 'keydown, keyup', 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 9: { 'Details': '{ code: Delete, which: 46 }', Typed: '{del}', 'Events Fired': `keydown, beforeinput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 10: { 'Details': '{ code: Enter, which: 13 }', Typed: '{enter}', 'Events Fired': `keydown, keypress, beforeinput, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
- 11: { 'Details': '{ code: MetaLeft, which: 91 }', Typed: '{cmd}', 'Events Fired': 'keyup', 'Active Modifiers': 'alt', 'Prevented Default': null, 'Target Element': $input[0] },
- 12: { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{option}', 'Events Fired': 'keyup', 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
- }
+ const expectedTable = [
+ { 'Details': '{ code: MetaLeft, which: 91 }', Typed: '{cmd}', 'Events Fired': 'keydown', 'Active Modifiers': 'meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{option}', 'Events Fired': 'keydown', 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: KeyF, which: 70 }', Typed: 'f', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: KeyO, which: 79 }', Typed: 'o', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: KeyO, which: 79 }', Typed: 'o', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: Enter, which: 13 }', Typed: '{enter}', 'Events Fired': `keydown, keypress, beforeinput, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: KeyB, which: 66 }', Typed: 'b', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: ArrowLeft, which: 37 }', Typed: '{leftarrow}', 'Events Fired': 'keydown, keyup', 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: Delete, which: 46 }', Typed: '{del}', 'Events Fired': `keydown, beforeinput, input, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: Enter, which: 13 }', Typed: '{enter}', 'Events Fired': `keydown, keypress, beforeinput, keyup`, 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: MetaLeft, which: 91 }', Typed: '{cmd}', 'Events Fired': 'keyup', 'Active Modifiers': 'alt', 'Prevented Default': null, 'Target Element': $input[0] },
+ { 'Details': '{ code: AltLeft, which: 18 }', Typed: '{option}', 'Events Fired': 'keyup', 'Active Modifiers': null, 'Prevented Default': null, 'Target Element': $input[0] },
+ ]
// uncomment for debugging
- // _.each(table.data, (v, i) => expect(v).containSubset(expectedTable[i]))
expect(table.data).to.deep.eq(expectedTable)
expect($input.val()).eq('foo')
})
@@ -2964,9 +3035,9 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
cy.get(':text:first').type('f').then(function ($el) {
const table = this.lastLog.invoke('consoleProps').table[2]()
- expect(table.data).to.deep.eq({
- 1: { Typed: 'f', 'Events Fired': `keydown, keypress, beforeinput, textInput, input, keyup`, 'Active Modifiers': null, Details: '{ code: KeyF, which: 70 }', 'Prevented Default': null, 'Target Element': $el[0] },
- })
+ expect(table.data).to.deep.eq([
+ { Typed: 'f', 'Events Fired': 'keydown, keypress, beforeinput, textInput, input, keyup', 'Active Modifiers': null, Details: '{ code: KeyF, which: 70 }', 'Prevented Default': null, 'Target Element': $el[0] },
+ ])
})
})
@@ -2978,12 +3049,9 @@ describe('src/cy/commands/actions/type - #type', { browser: '!webkit' }, () => {
cy.get(':text:first').type('f').then(function ($el) {
const table = this.lastLog.invoke('consoleProps').table[2]()
- // eslint-disable-next-line
- 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] },
- })
+ expect(table.data).to.deep.eq([
+ { Typed: 'f', 'Events Fired': 'keydown, keyup', 'Active Modifiers': null, Details: '{ code: KeyF, which: 70 }', 'Prevented Default': true, 'Target Element': $el[0] },
+ ])
})
})
})
diff --git a/packages/driver/cypress/e2e/commands/actions/type_errors.cy.js b/packages/driver/cypress/e2e/commands/actions/type_errors.cy.js
index 00225dc4c44d..b5c864ee7a4e 100644
--- a/packages/driver/cypress/e2e/commands/actions/type_errors.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/type_errors.cy.js
@@ -1,8 +1,7 @@
const { assertLogLength } = require('../../../support/utils')
const { _, $ } = Cypress
-// TODO(webkit): fix+unskip for experimental webkit release
-describe('src/cy/commands/actions/type - #type errors', { browser: '!webkit' }, () => {
+describe('src/cy/commands/actions/type - #type errors', () => {
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
diff --git a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
index 847ba0b64cf6..405857a55713 100644
--- a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js
@@ -6,8 +6,7 @@ const {
trimInnerText,
} = require('../../../support/utils')
-// TODO(webkit): fix+unskip for experimental webkit release
-describe('src/cy/commands/actions/type - #type special chars', { browser: '!webkit' }, () => {
+describe('src/cy/commands/actions/type - #type special chars', () => {
before(function () {
cy
.visit('/fixtures/dom.html')
@@ -114,7 +113,13 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
it('fires textInput event with e.data', (done) => {
cy.$$(':text:first').on('textInput', (e) => {
- expect(e.originalEvent.data).to.eq('{')
+ if (Cypress.isBrowser('webkit')) {
+ // For WebKit, the simulated textInput event is not
+ // emitted with the char data to prevent double entry.
+ expect(e.originalEvent.data).to.eq('')
+ } else {
+ expect(e.originalEvent.data).to.eq('{')
+ }
done()
})
@@ -309,8 +314,8 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
attachKeyListeners({ input })
cy.get(':text:first').invoke('val', 'ab')
- .then(($input) => $input[0].setSelectionRange(0, 0))
.focus()
+ .then(($input) => $input[0].setSelectionRange(0, 0))
.type('{backspace}')
.should('have.value', 'ab')
@@ -352,8 +357,8 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
attachKeyListeners({ input })
cy.get('textarea:first').invoke('val', 'ab')
- .then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.focus()
+ .then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.type('{backspace}')
.should('have.value', 'ab')
@@ -447,9 +452,8 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
attachKeyListeners({ input })
cy.get(':text:first').invoke('val', 'ab')
-
- .then(($input) => $input[0].setSelectionRange(0, 0))
.focus()
+ .then(($input) => $input[0].setSelectionRange(0, 0))
.type('{del}')
.should('have.value', 'b')
@@ -493,8 +497,8 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
attachKeyListeners({ textarea })
cy.get('textarea:first').invoke('val', 'ab')
- .then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.focus()
+ .then(($textarea) => $textarea[0].setSelectionRange(0, 0))
.type('{del}')
.should('have.value', 'b')
@@ -999,7 +1003,10 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
cy.get('input[type="number"]:first')
.invoke('val', '12.34')
.type('{uparrow}{uparrow}')
- .should('have.value', '14')
+ // WebKit does not round to the step value when calling stepUp/stepDown,
+ // as we do here for the ArrowUp handler.
+ // https://bugs.webkit.org/show_bug.cgi?id=244206
+ .should('have.value', Cypress.isBrowser('webkit') ? '14.34' : '14')
})
})
@@ -1057,7 +1064,10 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
cy.get('input[type="number"]:first')
.invoke('val', '12.34')
.type('{downarrow}{downarrow}')
- .should('have.value', '11')
+ // WebKit does not round to the step value when calling stepUp/stepDown,
+ // as we do here for the ArrowDown handler
+ // https://bugs.webkit.org/show_bug.cgi?id=244206
+ .should('have.value', Cypress.isBrowser('webkit') ? '10.34' : '11')
})
it('downarrow ignores current selection', () => {
@@ -1292,7 +1302,6 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
this.$forms.find('#single-input').submit((e) => {
e.preventDefault()
-
events.push('submit')
})
@@ -1736,7 +1745,8 @@ describe('src/cy/commands/actions/type - #type special chars', { browser: '!webk
})
})
- it('will not submit the form', function (done) {
+ // WebKit still emits the submit event on the form in this configuration.
+ it('will not submit the form', { browser: '!webkit' }, function (done) {
this.$forms.find('#multiple-inputs-and-multiple-submits').submit(() => {
done(new Error('should not receive submit event'))
})
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts
index 3d6819a00e74..23495075d7f7 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts
@@ -455,14 +455,14 @@ context('cy.origin actions', () => {
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
- expect(KeyboardEventsTable.data[1]).to.have.property('Details').that.equals('{ code: KeyF, which: 70 }')
- expect(KeyboardEventsTable.data[1]).to.have.property('Typed').that.equals('f')
+ expect(KeyboardEventsTable.data[0]).to.have.property('Details').that.equals('{ code: KeyF, which: 70 }')
+ expect(KeyboardEventsTable.data[0]).to.have.property('Typed').that.equals('f')
+
+ expect(KeyboardEventsTable.data[1]).to.have.property('Details').that.equals('{ code: KeyO, which: 79 }')
+ expect(KeyboardEventsTable.data[1]).to.have.property('Typed').that.equals('o')
expect(KeyboardEventsTable.data[2]).to.have.property('Details').that.equals('{ code: KeyO, which: 79 }')
expect(KeyboardEventsTable.data[2]).to.have.property('Typed').that.equals('o')
-
- expect(KeyboardEventsTable.data[3]).to.have.property('Details').that.equals('{ code: KeyO, which: 79 }')
- expect(KeyboardEventsTable.data[3]).to.have.property('Typed').that.equals('o')
})
})
diff --git a/packages/driver/src/cy/commands/actions/focus.ts b/packages/driver/src/cy/commands/actions/focus.ts
index e866def4c133..414c336c9891 100644
--- a/packages/driver/src/cy/commands/actions/focus.ts
+++ b/packages/driver/src/cy/commands/actions/focus.ts
@@ -4,6 +4,7 @@ import $dom from '../../../dom'
import $utils from '../../../cypress/utils'
import $errUtils from '../../../cypress/error_utils'
import $elements from '../../../dom/elements'
+import $selection from '../../../dom/selection'
import type { Log } from '../../../cypress/log'
interface InternalFocusOptions extends Partial {
@@ -89,6 +90,18 @@ export default (Commands, Cypress, cy) => {
cy.fireFocus(el)
+ if (Cypress.isBrowser('webkit') && (
+ $elements.isInput(el) || $elements.isTextarea(el)
+ )) {
+ // Force selection to end in WebKit, unless selection
+ // has been set by user.
+ // It's a curried function, so the 2 arguments are valid.
+ // @ts-ignore
+ $selection.moveSelectionToEnd(el, {
+ onlyIfEmptySelection: true,
+ })
+ }
+
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(options.$el, options, {
onRetry: verifyAssertions,
diff --git a/packages/driver/src/cy/commands/actions/selectFile.ts b/packages/driver/src/cy/commands/actions/selectFile.ts
index a946250bc529..946587b68894 100644
--- a/packages/driver/src/cy/commands/actions/selectFile.ts
+++ b/packages/driver/src/cy/commands/actions/selectFile.ts
@@ -20,7 +20,7 @@ import { addEventCoords, dispatch } from './trigger'
* override webkitGetAsEntry() throws an error.
*
*/
-const tryMockWebkit = (item) => {
+const tryMockWebKit = (item) => {
try {
item.webkitGetAsEntry = () => {
return {
@@ -58,7 +58,7 @@ const createDataTransfer = (files: Cypress.FileReferenceObject[], eventTarget: J
// also cannot be constructed, so we have to use an array instead.
Object.defineProperty(dataTransfer, 'items', {
get () {
- return _.map(oldItems, tryMockWebkit)
+ return _.map(oldItems, tryMockWebKit)
},
})
diff --git a/packages/driver/src/cy/commands/actions/type.ts b/packages/driver/src/cy/commands/actions/type.ts
index 9b083b55fdf8..e42400c62eb4 100644
--- a/packages/driver/src/cy/commands/actions/type.ts
+++ b/packages/driver/src/cy/commands/actions/type.ts
@@ -87,14 +87,8 @@ export default function (Commands, Cypress, cy, state, config) {
}
}
- // transform table object into object with zero based index as keys
const getTableData = () => {
- return _.reduce(_.values(table), (memo, value, index) => {
- memo[index + 1] = value
-
- return memo
- }
- , {})
+ return _.values(table)
}
options._log = Cypress.log({
@@ -255,7 +249,10 @@ export default function (Commands, Cypress, cy, state, config) {
// when we send {Enter} KeyboardEvent to the input fields.
// Because of that, we don't have to click the submit buttons.
// Otherwise, we trigger submit events twice.
- if (!isFirefoxBefore98) {
+ //
+ // WebKit will send the submit with an Enter keypress event,
+ // so we do need to click the default button in this case.
+ if (!isFirefoxBefore98 && !Cypress.isBrowser('webkit')) {
// issue the click event to the 'default button' of the form
// we need this to be synchronous so not going through our
// own click command
diff --git a/packages/driver/src/cy/focused.ts b/packages/driver/src/cy/focused.ts
index d1592919bbfa..730cac177c6a 100644
--- a/packages/driver/src/cy/focused.ts
+++ b/packages/driver/src/cy/focused.ts
@@ -123,12 +123,17 @@ export const create = (state: StateFunc) => ({
const $focused = this.getFocused(el.ownerDocument)
let hasFocused = false
+ let onFocusCapture
// we need to bind to the focus event here
// because some browsers will not ever fire
// the focus event if the window itself is not
// currently focused
const cleanup = () => {
+ if (onFocusCapture) {
+ $elements.callNativeMethod(el, 'removeEventListener', 'focus', onFocusCapture, { capture: true })
+ }
+
return $elements.callNativeMethod(el, 'removeEventListener', 'focus', onFocus)
}
@@ -136,6 +141,48 @@ export const create = (state: StateFunc) => ({
return hasFocused = true
}
+ if (Cypress.isBrowser('webkit')) {
+ // By default, WebKit will select the contents of an input element when the input is focused.
+ // This is problematic, as we use the existence of any selection to determine whether
+ // we adjust the input's cursor and prepare the input for receiving additional content.
+ // Without intervention, we will always interpret this default selection as a user-performed selection
+ // and persist it, leaving the selection contents to be overwritten rather than appended to
+ // on subsequent actions.
+ //
+ // In order to avoid this behavior, we use a focus event during the capture phase to set
+ // our own initial blank selection. This short-circuits WebKit's default behavior and ensures
+ // that any user-performed selections performed during the focus event's bubble phase are still applied.
+
+ onFocusCapture = (event: FocusEvent) => {
+ const eventTarget = event.currentTarget as HTMLInputElement
+
+ if (!eventTarget.setSelectionRange) {
+ return
+ }
+
+ try {
+ // Prior to being focused, the element's selectionStart/End will be at 0.
+ // Even so, we need to explicitly call setSelectionRange here to prevent WebKit
+ // from selecting the contents after being focused.
+ //
+ // By re-setting the selection at the current start/end values,
+ // we ensure that any selection values set by previous event handlers
+ // are persisted.
+ eventTarget.setSelectionRange(
+ eventTarget.selectionStart,
+ eventTarget.selectionEnd,
+ )
+ } catch (e) {
+ // Some input types do not support selections and will throw when
+ // setSelectionRange is called. We can ignore these errors,
+ // as these elements wouldn't have a selection to we need to
+ // prevent anyway.
+ }
+ }
+
+ $elements.callNativeMethod(el, 'addEventListener', 'focus', onFocusCapture, { capture: true })
+ }
+
$elements.callNativeMethod(el, 'addEventListener', 'focus', onFocus)
$elements.callNativeMethod(el, 'focus', opts)
diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts
index 485687fb13e0..6bf3660a5f89 100644
--- a/packages/driver/src/cy/keyboard.ts
+++ b/packages/driver/src/cy/keyboard.ts
@@ -912,7 +912,18 @@ export class Keyboard {
keyCode = 0
which = 0
location = undefined
- data = text === '\r' ? '↵' : text
+
+ // WebKit will insert characters on a textInput event, resulting
+ // in double char entry when the default handler is executed. But values
+ // inserted by textInput aren't always correct/aren't filtered
+ // through our shouldUpdateValue logic, so we prevent textInput's
+ // default logic by removing the key data from the event.
+ if (Cypress.isBrowser('webkit')) {
+ data = ''
+ } else {
+ data = text === '\r' ? '↵' : text
+ }
+
break
case 'beforeinput':
@@ -1162,6 +1173,13 @@ export class Keyboard {
if ($elements.isContentEditable(elToType)) {
key.events.input = false
+
+ if (Cypress.isBrowser('webkit')) {
+ // WebKit will emit beforeinput itself when the text is
+ // inserted into a contenteditable input using `execCommand('insertText')`.
+ // We prevent the simulated event from firing to avoid duplicative events.
+ key.events.beforeinput = false
+ }
} else if ($elements.isReadOnlyInputOrTextarea(elToType)) {
key.events.textInput = false
}
diff --git a/packages/driver/src/cy/mouse.ts b/packages/driver/src/cy/mouse.ts
index 3ba3ac914881..2958b03881a8 100644
--- a/packages/driver/src/cy/mouse.ts
+++ b/packages/driver/src/cy/mouse.ts
@@ -57,6 +57,7 @@ type DefaultMouseOptions = ModifiersEventOptions & CoordsEventOptions & {
export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused, Cypress: ICypress) => {
const isFirefox = Cypress.browser.family === 'firefox'
+ const isWebKit = Cypress.isBrowser('webkit')
const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => {
const constructor = el.ownerDocument.defaultView.PointerEvent
@@ -72,14 +73,14 @@ export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused,
}
const sendPointerup = (el, evtOptions) => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
return sendPointerEvent(el, evtOptions, 'pointerup', true, true)
}
const sendPointerdown = (el, evtOptions): {} | SentEvent => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
@@ -102,14 +103,14 @@ export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused,
}
const sendMouseup = (el, evtOptions) => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
return sendMouseEvent(el, evtOptions, 'mouseup', true, true)
}
const sendMousedown = (el, evtOptions): {} | SentEvent => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
@@ -132,21 +133,21 @@ export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused,
}
const sendClick = (el, evtOptions, opts: { force?: boolean } = {}) => {
// send the click event if firefox and force (needed for force check checkbox)
- if (!opts.force && isFirefox && el.disabled) {
+ if (!opts.force && (isFirefox || isWebKit) && el.disabled) {
return {}
}
return sendMouseEvent(el, evtOptions, 'click', true, true)
}
const sendDblclick = (el, evtOptions) => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
return sendMouseEvent(el, evtOptions, 'dblclick', true, true)
}
const sendContextmenu = (el, evtOptions) => {
- if (isFirefox && el.disabled) {
+ if ((isFirefox || isWebKit) && el.disabled) {
return {}
}
diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts
index 649a3cfabd2a..a4ad9af6593a 100644
--- a/packages/driver/src/dom/selection.ts
+++ b/packages/driver/src/dom/selection.ts
@@ -575,19 +575,7 @@ const _moveSelectionTo = function (toStart: boolean, el: HTMLElement, options =
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
}
} else {
- let range
-
- // Sometimes, selection.rangeCount is 0 when there is no selection.
- // In that case, it fails in Chrome.
- // We're creating a new range and add it to the selection to avoid the case.
- if (selection.rangeCount === 0) {
- range = doc.createRange()
- selection.addRange(range)
- } else {
- range = selection.getRangeAt(0)
- }
-
- range.selectNodeContents(el)
+ selection.selectAllChildren(el)
}
toStart ? selection.collapseToStart() : selection.collapseToEnd()
diff --git a/packages/server/lib/browsers/webkit-automation.ts b/packages/server/lib/browsers/webkit-automation.ts
index 0ebac526187a..cccf6b52caa7 100644
--- a/packages/server/lib/browsers/webkit-automation.ts
+++ b/packages/server/lib/browsers/webkit-automation.ts
@@ -83,7 +83,7 @@ const _cookieMatches = (cookie: any, filter: Record) => {
let requestIdCounter = 1
const requestIdMap = new WeakMap()
-export class WebkitAutomation {
+export class WebKitAutomation {
private context!: playwright.BrowserContext
private page!: playwright.Page
@@ -91,7 +91,7 @@ export class WebkitAutomation {
// static initializer to avoid "not definitively declared"
static async create (automation: Automation, browser: playwright.Browser, initialUrl: string) {
- const wkAutomation = new WebkitAutomation(automation, browser)
+ const wkAutomation = new WebKitAutomation(automation, browser)
await wkAutomation.reset(initialUrl)
diff --git a/packages/server/lib/browsers/webkit.ts b/packages/server/lib/browsers/webkit.ts
index e236a7ecfcf4..58c7661856c6 100644
--- a/packages/server/lib/browsers/webkit.ts
+++ b/packages/server/lib/browsers/webkit.ts
@@ -3,11 +3,11 @@ import { EventEmitter } from 'events'
import type playwright from 'playwright-webkit'
import type { Browser, BrowserInstance } from './types'
import type { Automation } from '../automation'
-import { WebkitAutomation } from './webkit-automation'
+import { WebKitAutomation } from './webkit-automation'
const debug = Debug('cypress:server:browsers:webkit')
-let wkAutomation: WebkitAutomation | undefined
+let wkAutomation: WebKitAutomation | undefined
export async function connectToNewSpec (browser: Browser, options, automation: Automation) {
if (!wkAutomation) throw new Error('connectToNewSpec called without wkAutomation')
@@ -31,7 +31,7 @@ export async function open (browser: Browser, url, options: any = {}, automation
headless: browser.isHeadless,
})
- wkAutomation = await WebkitAutomation.create(automation, pwBrowser, url)
+ wkAutomation = await WebKitAutomation.create(automation, pwBrowser, url)
automation.use(wkAutomation)
class WkInstance extends EventEmitter implements BrowserInstance {