Skip to content

Commit

Permalink
feat: can dispatch events with the type they want. (cypress-io#8305)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
  • Loading branch information
sainthkh and chrisbreiding authored Aug 31, 2020
1 parent 2469473 commit 15fdf12
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 4 deletions.
6 changes: 6 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2916,6 +2916,12 @@ declare namespace Cypress {
* @default true
*/
cancelable: boolean
/**
* The type of the event you want to trigger
*
* @default 'Event'
*/
eventConstructor: string
}

/** Options to change the default behavior of .writeFile */
Expand Down
21 changes: 21 additions & 0 deletions packages/driver/cypress/fixtures/issue-5650.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Test input event</title>
</head>
<body>
<input id="test-input"/>
<div id="result"></div>
</body>
<script>
let elem = window.document.getElementById('test-input');
let resultDiv = window.document.getElementById('result');
elem.addEventListener('keydown', (event) => {
resultDiv.innerText = `isKeyboardEvent: ${event instanceof KeyboardEvent}`;
});
elem.addEventListener('mousedown', (event) => {
resultDiv.innerText = `isMouseEvent: ${event instanceof MouseEvent}`;
});
</script>
</html>
104 changes: 104 additions & 0 deletions packages/driver/cypress/integration/commands/actions/trigger_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,27 @@ describe('src/cy/commands/actions/trigger', () => {
cy.window().should('have.length.gt', 1).trigger('click')
})

// https://github.com/cypress-io/cypress/issues/3686
it('view should be AUT window', (done) => {
cy.window().then((win) => {
cy.get('input:first').then((jQueryElement) => {
let elem = jQueryElement.get(0)

elem.addEventListener('mousedown', (event) => {
expect(event.view).to.eql(win)
done()
})
})
})

cy.get('input:first').trigger('mousedown', {
eventConstructor: 'MouseEvent',
button: 0,
shiftKey: false,
ctrlKey: false,
})
})

describe('actionability', () => {
it('can trigger on elements which are hidden until scrolled within parent container', () => {
cy.get('#overflow-auto-container').contains('quux').trigger('mousedown')
Expand Down Expand Up @@ -742,6 +763,76 @@ describe('src/cy/commands/actions/trigger', () => {
})
})

// https://github.com/cypress-io/cypress/issues/5650
describe('dispatches correct Event objects', () => {
it('should trigger KeyboardEvent with .trigger inside Cypress event listener', (done) => {
cy.window().then((win) => {
cy.get('input:first').then((jQueryElement) => {
let elemHtml = jQueryElement.get(0)

elemHtml.addEventListener('keydown', (event) => {
expect(event instanceof win['KeyboardEvent']).to.be.true
done()
})
})
})

cy.get('input:first').trigger('keydown', {
eventConstructor: 'KeyboardEvent',
keyCode: 65,
which: 65,
shiftKey: false,
ctrlKey: false,
})
})

it('should trigger KeyboardEvent with .trigger inside html script event listener', () => {
cy.visit('fixtures/issue-5650.html')

cy.get('#test-input').trigger('keydown', {
eventConstructor: 'KeyboardEvent',
keyCode: 65,
which: 65,
shiftKey: false,
ctrlKey: false,
})

cy.get('#result').contains('isKeyboardEvent: true')
})

it('should trigger MouseEvent with .trigger inside Cypress event listener', (done) => {
cy.window().then((win) => {
cy.get('input:first').then((jQueryElement) => {
let elem = jQueryElement.get(0)

elem.addEventListener('mousedown', (event) => {
expect(event instanceof win['MouseEvent']).to.be.true
done()
})
})
})

cy.get('input:first').trigger('mousedown', {
eventConstructor: 'MouseEvent',
button: 0,
shiftKey: false,
ctrlKey: false,
})
})

it('should trigger MouseEvent with .trigger inside html script event listener', () => {
cy.visit('fixtures/issue-5650.html')
cy.get('#test-input').trigger('mousedown', {
eventConstructor: 'MouseEvent',
button: 0,
shiftKey: false,
ctrlKey: false,
})

cy.get('#result').contains('isMouseEvent: true')
})
})

describe('errors', {
defaultCommandTimeout: 100,
}, () => {
Expand Down Expand Up @@ -864,6 +955,19 @@ describe('src/cy/commands/actions/trigger', () => {
cy.get('button:first').trigger('mouseover', 'foo')
})

it('throws when provided invalid event type', function (done) {
cy.on('fail', (err) => {
expect(this.logs.length).to.eq(2)
expect(err.message).to.eq('Timed out retrying: `cy.trigger()` `eventConstructor` option must be a valid event (e.g. \'MouseEvent\', \'KeyboardEvent\'). You passed: `FooEvent`')

done()
})

cy.get('button:first').trigger('mouseover', {
eventConstructor: 'FooEvent',
})
})

it('throws when element animation exceeds timeout', (done) => {
// force the animation calculation to think we moving at a huge distance ;-)
cy.stub(Cypress.utils, 'getDistanceBetween').returns(100000)
Expand Down
29 changes: 25 additions & 4 deletions packages/driver/src/cy/commands/actions/trigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,29 @@ const $dom = require('../../../dom')
const $errUtils = require('../../../cypress/error_utils')
const $actionability = require('../../actionability')

const dispatch = (target, eventName, options) => {
const event = new Event(eventName, options)
const dispatch = (target, appWindow, eventName, options) => {
const eventConstructor = options.eventConstructor ?? 'Event'
const ctor = appWindow[eventConstructor]

if (typeof ctor !== 'function') {
$errUtils.throwErrByPath('trigger.invalid_event_type', {
args: { eventConstructor },
})
}

// eventConstructor property should not be added to event instance.
delete options.eventConstructor

// https://github.com/cypress-io/cypress/issues/3686
// UIEvent and its derived events like MouseEvent, KeyboardEvent
// has a property, view, which is the window object where the event happened.
// Logic below checks the ctor function is UIEvent itself or its children
// and adds view to the instance init object.
if (ctor === appWindow['UIEvent'] || ctor.prototype instanceof appWindow['UIEvent']) {
options.view = appWindow
}

const event = new ctor(eventName, options)

// some options, like clientX & clientY, must be set on the
// instance instead of passing them into the constructor
Expand Down Expand Up @@ -85,7 +106,7 @@ module.exports = (Commands, Cypress, cy, state, config) => {

const trigger = () => {
if (dispatchEarly) {
return dispatch(subject, eventName, eventOptions)
return dispatch(subject, state('window'), eventName, eventOptions)
}

return $actionability.verify(cy, subject, options, {
Expand All @@ -112,7 +133,7 @@ module.exports = (Commands, Cypress, cy, state, config) => {
pageY: fromElWindow.y,
}, eventOptions)

return dispatch($elToClick.get(0), eventName, eventOptions)
return dispatch($elToClick.get(0), state('window'), eventName, eventOptions)
},
})
}
Expand Down
4 changes: 4 additions & 0 deletions packages/driver/src/cypress/error_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,10 @@ module.exports = {
message: `${cmd('trigger')} can only be called on a single element. Your subject contained {{num}} elements.`,
docsUrl: 'https://on.cypress.io/trigger',
},
invalid_event_type: {
message: `${cmd('trigger')} \`eventConstructor\` option must be a valid event (e.g. 'MouseEvent', 'KeyboardEvent'). You passed: \`{{eventConstructor}}\``,
docsUrl: 'https://on.cypress.io/trigger',
},
},

type: {
Expand Down

0 comments on commit 15fdf12

Please # to comment.