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

feat: support beforeinput event. #8411

Merged
merged 22 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2810,6 +2810,14 @@ declare namespace Cypress {
* @default true
*/
release: boolean
/**
* An event object is sent but the text field isn't updated by Cypress.
* It is necessary with some editor libraries like slate.js that
* updates its own text state.
*
* @default false
*/
noUpdate: boolean
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/driver/cypress/fixtures/issue-7088.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<title>Slatejs App</title>
</head>
<body>
<div id="root"></div>
<script>!function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c<f.length;c++)l=f[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,f=1;f<t.length;f++){var i=t[f];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var f=this.webpackJsonp7088=this.webpackJsonp7088||[],i=f.push.bind(f);f.push=r,f=f.slice();for(var a=0;a<f.length;a++)r(f[a]);var p=i;t()}([])</script>
<!-- Compiled and minified version of https://www.slatejs.org/examples/plaintext -->
<script src="issue-7088.js"></script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/driver/cypress/fixtures/issue-7088.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2995,14 +2995,14 @@ describe('src/cy/commands/actions/type - #type', () => {
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, 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, 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, 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, keyup', 'Active Modifiers': 'alt, meta', 'Prevented Default': null, 'Target Element': $input[0] },
7: { 'Details': '{ code: KeyB, which: 66 }', Typed: 'b', 'Events Fired': 'keydown, keypress, textInput, input, keyup', '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, 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, 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] },
}
Expand All @@ -3019,7 +3019,7 @@ describe('src/cy/commands/actions/type - #type', () => {
const table = this.lastLog.invoke('consoleProps').table[2]()

expect(table.data).to.deep.eq({
1: { Typed: 'f', 'Events Fired': 'keydown, keypress, textInput, input, keyup', 'Active Modifiers': null, Details: '{ code: KeyF, which: 70 }', 'Prevented Default': null, 'Target Element': $el[0] },
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] },
})
})
})
Expand Down
20 changes: 20 additions & 0 deletions packages/driver/cypress/integration/issues/7088_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference types="cypress" />

describe('slatejs', () => {
it('test', () => {
cy.visit('fixtures/issue-7088.html')

const testString = 'Hello world'

cy.get('[data-slate-editor="true"]')
.type(testString, { noUpdate: true })

cy.contains('[data-slate-string="true"]', testString)
.should('be.visible')

cy.get('[data-slate-editor="true"]')
.type('{ctrl}{shift}{backspace}', { release: false, noUpdate: true })
Copy link
Contributor

Choose a reason for hiding this comment

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

I tried removing the noUpdate options here and the test still passed, is there a test case that fails w/o the option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. noUpdate isn't necessary.

I added it because there were some cases when cy.type() breaks the html structure inside slatejs. It seems that it was because beforeinput part was buggy.

Copy link
Contributor

Choose a reason for hiding this comment

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

the reason you previously needed noUpdate was due to the event not being set as cancelable https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event, fixed now so we will respect a cancelled event and not perform the default action


cy.get('span[contenteditable="false"]').should('have.text', 'Enter some plain text...')
})
})
2 changes: 2 additions & 0 deletions packages/driver/src/cy/commands/actions/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = function (Commands, Cypress, cy, state, config) {
delay: 10,
release: true,
parseSpecialCharSequences: true,
noUpdate: false,
sainthkh marked this conversation as resolved.
Show resolved Hide resolved
waitForAnimations: config('waitForAnimations'),
animationDistanceThreshold: config('animationDistanceThreshold'),
})
Expand Down Expand Up @@ -248,6 +249,7 @@ module.exports = function (Commands, Cypress, cy, state, config) {
parseSpecialCharSequences: options.parseSpecialCharSequences,
window: win,
force: options.force,
noUpdate: options.noUpdate,
onFail: options._log,

updateValue (el, key, charsToType) {
Expand Down
92 changes: 83 additions & 9 deletions packages/driver/src/cy/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type KeyEventType =
| 'keypress'
| 'input'
| 'textInput'
| 'beforeinput'

const toModifiersEventOptions = (modifiers: KeyboardModifiers) => {
return {
Expand Down Expand Up @@ -489,7 +490,7 @@ function _getEndIndex (str, substr) {
const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = {
Enter: (el, key, options) => {
// if input element, Enter key does not insert text
if (!$elements.isInput(el)) {
if (!options.noUpdate && !$elements.isInput(el)) {
$selection.replaceSelectionContents(el, '\n')
}

Expand Down Expand Up @@ -593,6 +594,7 @@ export interface typeOptions {
release?: boolean
_log?: any
delay?: number
noUpdate?: boolean
onError?: Function
onEvent?: Function
onBeforeEvent?: Function
Expand Down Expand Up @@ -801,6 +803,7 @@ export class Keyboard {
let eventConstructor = 'KeyboardEvent'
let cancelable = true
let addModifiers = true
let inputType = ''
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this will add the inputType to every event since it's not initialized as undefined. Since we don't test for extra properties we should add a test for that.

I'll take a look

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. It should be written in that way.


switch (eventType) {
case 'keydown':
Expand Down Expand Up @@ -832,6 +835,14 @@ export class Keyboard {
data = text
break

case 'beforeinput':
eventConstructor = 'InputEvent'
addModifiers = false
data = text
location = undefined
cancelable = false
inputType = this.getInputType(keyDetails.code)
break
case 'input':
eventConstructor = 'InputEvent'
addModifiers = false
Expand Down Expand Up @@ -875,6 +886,7 @@ export class Keyboard {
data,
detail: 0,
view: win,
inputType,
},
_.isUndefined,
),
Expand Down Expand Up @@ -902,8 +914,8 @@ export class Keyboard {
} else {
// For some reason we can't set certain props on Keyboard Events in chrome < 63.
// So we'll use the plain Event constructor
// event = new win[eventConstructor](eventType, eventOptions)
event = new win['Event'](eventType, eventOptions)
event = new win[eventConstructor](eventType, eventOptions)
// event = new win['Event'](eventType, eventOptions)
_.extend(event, eventOptions)
}

Expand All @@ -917,6 +929,56 @@ export class Keyboard {
return dispatched
}

getInputType (code) {
const { shift, ctrl } = this.getActiveModifiers()

if (shift && ctrl && code === 'Delete') {
return 'deleteHardLineForward'
}

if (ctrl && code === 'Delete') {
return 'deleteWordForward'
}

if (code === 'Delete') {
return 'deleteContentForward'
}

if (shift && ctrl && code === 'Backspace') {
return 'deleteHardLineBackward'
}

if (ctrl && code === 'Backspace') {
return 'deleteWordBackward'
}

if (code === 'Backspace') {
return 'deleteContentBackward'
}

if (code === 'Enter') {
return 'insertLineBreak'
}

if (ctrl && code === 'KeyV') {
return 'insertFromPaste'
}

if (ctrl && code === 'KeyX') {
return 'deleteByCut'
}

if (ctrl && code === 'KeyZ') {
return 'historyUndo'
}

if (ctrl && code === 'KeyY') {
return 'historyRedo'
}

return 'insertText'
}

getActiveModifiers () {
return _.clone(this.state('keyboardModifiers')) || _.clone(INITIAL_MODIFIERS)
}
Expand Down Expand Up @@ -982,9 +1044,14 @@ export class Keyboard {
if (!key.text) {
key.events.keypress = false
key.events.textInput = false
key.events.beforeinput = false
if (key.key !== 'Backspace' && key.key !== 'Delete') {
key.events.input = false
}

if (key.key === 'Backspace' || key.key === 'Delete') {
key.events.beforeinput = true
}
}

let elToType
Expand Down Expand Up @@ -1018,10 +1085,15 @@ export class Keyboard {
this.fireSimulatedEvent(elToType, 'keypress', key, options)
) {
if (
shouldIgnoreEvent('textInput', key.events) ||
this.fireSimulatedEvent(elToType, 'textInput', key, options)
shouldIgnoreEvent('beforeinput', key.events) ||
this.fireSimulatedEvent(elToType, 'beforeinput', key, options)
) {
return this.performSimulatedDefault(elToType, key, options)
if (
shouldIgnoreEvent('textInput', key.events) ||
this.fireSimulatedEvent(elToType, 'textInput', key, options)
) {
return this.performSimulatedDefault(elToType, key, options)
}
}
}
}
Expand Down Expand Up @@ -1082,10 +1154,12 @@ export class Keyboard {
return
}

// noop if not in a text-editable
const ret = $selection.replaceSelectionContents(el, key.text)
if (!options.noUpdate) {
// noop if not in a text-editable
const ret = $selection.replaceSelectionContents(el, key.text)

debug('replaceSelectionContents:', key.text, ret)
debug('replaceSelectionContents:', key.text, ret)
}
}
}

Expand Down