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: can dispatch events with the type they want. #8305

Merged
merged 13 commits into from
Aug 31, 2020
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