UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

752 lines (580 loc) 21.1 kB
/* eslint arrow-body-style:'off' */ /** * in all browsers... * * activeElement is always programmatically respected and behaves identical whether window is in or out of focus * * browser: chrome... * * scenario 1: given '#one' is activeElement call programmatic el.focus() on '#two' * - if window is in focus * - blur will fire on '#one' * - focus will fire on '#two' * - if window is out of focus (the event wil be primed until the window receives focus again) * - by clicking anywhere on the <body> (not on the element)... * - focus on '#two' will fire first * - blur on '#two' will fire second * - activeElement will now be <body> * - by clicking on another element that is focusable * - focus on '#two' is first sent * - blur on '#two' is then sent * - focus is finally sent on the new focusable element we clicked * - if instead on clicking we programmatically call .focus() back to '#one' * - focus is fired on '#one' * - if we were to instead click directly on '#one' then no focus or blur events are fired * - if when clicking directly back to '#one' we prevent the 'mousedown' event * - the focus event will fire AND the element will still be activeElement * - had we not programmatically call .focus() ahead of time, then the focus event would * have been not fired, and our activeElement would not have changed * * scenario 2 : given '#one' is activeElement call programmatic el.blur() on '#one' * - if window is in focus * - blur will fire on '#one' * - if window is out of focus * - no events will ever fire even when regaining focus * browser: firefox... * - no focus events are queued when programmatically calling element.focus() AND the window is out of focus. the events evaporate into the ether. * - however, if calling window.focus() programmatically prior to programmatic element.focus() calls will fire all events as if the window is natively in focus */ const { _ } = Cypress const chaiSubset = require('chai-subset') chai.use(chaiSubset) const windowHasFocus = function () { if (top.document.hasFocus()) return true let hasFocus = false window.addEventListener('focus', function () { hasFocus = true }) window.focus() return hasFocus } const requireWindowInFocus = () => { let hasFocus = windowHasFocus() if (!hasFocus) { expect(hasFocus, 'this test requires the window to be in focus').ok } } it('can intercept blur/focus events', () => { // Browser must be in focus const focus = cy.spy(window.top.HTMLElement.prototype, 'focus') const blur = cy.spy(window.top.HTMLElement.prototype, 'blur') const handleFocus = cy.stub().as('handleFocus') const handleBlur = cy.stub().as('handleBlur') const resetStubs = () => { focus.resetHistory() blur.resetHistory() handleFocus.resetHistory() handleBlur.resetHistory() } cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { requireWindowInFocus() expect(cy.getFocused()).to.be.null // programmatically focus the first, then second input element const one = cy.$$('#one')[0] const two = cy.$$('#two')[0] one.addEventListener('focus', handleFocus) two.addEventListener('focus', handleFocus) one.addEventListener('blur', handleBlur) two.addEventListener('blur', handleBlur) one.focus() expect(focus).to.calledOnce expect(handleFocus).calledOnce expect(blur).not.called expect(handleBlur).not.called resetStubs() one.focus() expect(focus).to.calledOnce expect(handleFocus).not.called expect(blur).not.called expect(handleBlur).not.called resetStubs() one.blur() expect(blur).calledOnce expect(handleBlur).calledOnce resetStubs() one.blur() expect(blur).calledOnce expect(handleBlur).not.called }) }) it('blur the activeElement when clicking the body', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { const events = [] expect(cy.getFocused()).to.be.null const doc = cy.state('document') // programmatically focus the first, then second input element const $body = cy.$$('body') const $one = cy.$$('#one') const $two = cy.$$('#two'); ['focus', 'blur'].forEach((evt) => { $one.on(evt, (e) => { events.push(e.originalEvent) }) $two.on(evt, (e) => { events.push(e.originalEvent) }) }) $one.get(0).focus() $two.get(0).focus() cy.then(() => { // if we currently have focus it means // that the browser should fire the // native event immediately expect(events).to.have.length(3) expect(_.toPlainObject(events[0])).to.include({ type: 'focus', isTrusted: true, target: $one.get(0), }) expect(_.toPlainObject(events[1])).to.include({ type: 'blur', isTrusted: true, target: $one.get(0), }) expect(_.toPlainObject(events[2])).to.include({ type: 'focus', isTrusted: true, target: $two.get(0), }) }) cy .get('body').click() .then(() => { expect(doc.activeElement).to.eq($body.get(0)) }) cy.then(() => { // if we had focus then no additional // focus event is necessary expect(events).to.have.length(4) expect(_.toPlainObject(events[3])).to.include({ type: 'blur', isTrusted: true, target: $two.get(0), }) }) }) }) describe('polyfill programmatic blur events', () => { // restore these props for the rest of the tests let stubElementFocus let stubElementBlur let stubSVGFocus let stubSVGBlur let stubHasFocus let oldActiveElement = null const setActiveElement = (el) => { Object.defineProperty(cy.state('document'), 'activeElement', { get () { return el }, configurable: true, }) } beforeEach(() => { oldActiveElement = Object.getOwnPropertyDescriptor(window.Document.prototype, 'activeElement') // simulate window being out of focus by overwriting // the focus/blur methods on HTMLElement stubHasFocus = cy.stub(window.top.document, 'hasFocus').returns(false) stubElementFocus = cy.stub(window.top.HTMLElement.prototype, 'focus') stubElementBlur = cy.stub(window.top.HTMLElement.prototype, 'blur') stubSVGFocus = cy.stub(window.top.SVGElement.prototype, 'focus') stubSVGBlur = cy.stub(window.top.SVGElement.prototype, 'blur') }) afterEach(() => { Object.defineProperty(window.Document.prototype, 'activeElement', oldActiveElement) stubHasFocus.restore() stubElementFocus.restore() stubElementBlur.restore() stubSVGFocus.restore() stubSVGBlur.restore() }) // https://github.com/cypress-io/cypress/issues/1486 it('simulated events when window is out of focus when .focus called', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { // programmatically focus the first, then second input element const $one = cy.$$('#one') const $two = cy.$$('#two') const stub = cy.stub().as('focus/blur event').callsFake(() => { Cypress.log({}) }); ['focus', 'blur'].forEach((evt) => { $one.on(evt, stub) return $two.on(evt, stub) }) $one.get(0).focus() // a hack here becuase we nuked the real .focus setActiveElement($one.get(0)) $two.get(0).focus() // cy.get('#two').click() const getEvent = (n) => { return stub.getCall(n).args[0].originalEvent } cy.wrap(null).then(() => { expect(stub).to.be.calledThrice expect(getEvent(0)).to.containSubset({ type: 'focus', target: $one.get(0), isTrusted: false, }) expect(getEvent(1)).to.containSubset({ type: 'blur', target: $one.get(0), isTrusted: false, }) expect(getEvent(2)).to.containSubset({ type: 'focus', target: $two.get(0), isTrusted: false, }) }) .then(() => { stub.resetHistory() setActiveElement($two.get(0)) $two.get(0).focus() expect(stub, 'should not send focus if already focused el').not.called }) }) }) // https://github.com/cypress-io/cypress/issues/1176 it('simulated events when window is out of focus when .blur called', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { // programmatically focus the first, then second input element const $one = cy.$$('#one') const $two = cy.$$('#two') const stub = cy.stub().as('focus/blur event'); ['focus', 'blur'].forEach((evt) => { $one.on(evt, stub) $two.on(evt, stub) }) $one.get(0).focus() // a hack here becuase we nuked the real .focus setActiveElement($one.get(0)) $one.get(0).blur() cy.then(() => { expect(stub).calledTwice expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({ type: 'focus', target: $one.get(0), isTrusted: false, }) expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({ type: 'blur', target: $one.get(0), isTrusted: false, }) }) .then(() => { stub.resetHistory() setActiveElement(cy.$$('body').get(0)) $one.get(0).blur() expect(stub, 'should not send blur if not focused el').not.called }) }) }) // https://github.com/cypress-io/cypress/issues/1486 it('SVGElement simulated events when window is out of focus when .focus called', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { // programmatically focus the first, then second input element const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> </svg>`).appendTo(cy.$$('body')) const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> </svg>`).appendTo(cy.$$('body')) const stub = cy.stub().as('focus/blur event').callsFake(() => { Cypress.log({}) }); ['focus', 'blur'].forEach((evt) => { $one.on(evt, stub) return $two.on(evt, stub) }) $one.get(0).focus() // a hack here becuase we nuked the real .focus setActiveElement($one.get(0)) $two.get(0).focus() // cy.get('#two').click() const getEvent = (n) => { return stub.getCall(n).args[0].originalEvent } cy.wrap(null).then(() => { expect(stub).to.be.calledThrice expect(getEvent(0)).to.containSubset({ type: 'focus', target: $one.get(0), isTrusted: false, }) expect(getEvent(1)).to.containSubset({ type: 'blur', target: $one.get(0), isTrusted: false, }) expect(getEvent(2)).to.containSubset({ type: 'focus', target: $two.get(0), isTrusted: false, }) }) .then(() => { stub.resetHistory() setActiveElement($two.get(0)) $two.get(0).focus() expect(stub, 'should not send focus if already focused el').not.called }) }) }) // https://github.com/cypress-io/cypress/issues/1176 it('SVGElement simulated events when window is out of focus when .blur called', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { // programmatically focus the first, then second input element const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> </svg>`).appendTo(cy.$$('body')) const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> </svg>`).appendTo(cy.$$('body')) const stub = cy.stub().as('focus/blur event'); ['focus', 'blur'].forEach((evt) => { $one.on(evt, stub) $two.on(evt, stub) }) $one.get(0).focus() // a hack here becuase we nuked the real .focus setActiveElement($one.get(0)) $one.get(0).blur() cy.then(() => { expect(stub).calledTwice expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({ type: 'focus', target: $one.get(0), isTrusted: false, }) expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({ type: 'blur', target: $one.get(0), isTrusted: false, }) }) .then(() => { stub.resetHistory() setActiveElement(cy.$$('body').get(0)) $one.get(0).blur() expect(stub, 'should not send blur if not focused el').not.called }) }) }) it('document.hasFocus() always returns true', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html') cy.document().then((doc) => { expect(doc.hasFocus(), 'hasFocus returns true').eq(true) }) }) it('does not send focus events for non-focusable elements', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { cy.$$('<div id="no-focus">clearly not a focusable element</div>') .appendTo(cy.$$('body')) const stub = cy.stub() const el1 = cy.$$('#no-focus') const win = cy.$$(cy.state('window')) win.on('focus', stub) el1.on('focus', stub) el1[0].focus() expect(stub).not.called }) }) }) describe('intercept blur methods correctly', () => { beforeEach(() => { cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => { top.focus() cy.$$('input:first')[0].focus() cy.document().then((doc) => { // we need to wait for initial selectionchange event on input:first // which sets up the state for the tests // NOTE: initial selectionchange event does not fire in firefox if (!Cypress.isBrowser({ family: 'firefox' })) { return new Promise((resolve) => doc.onselectionchange = resolve) } }) cy.document().then((doc) => { doc.onselectionchange = cy.stub() .as('selectionchange') }) }) }) it('focus <a>', () => { const $el = cy.$$('<a href="#">foo</a>') $el.appendTo(cy.$$('body')) cy.wrap($el[0]).focus() .should('have.focus') if (Cypress.isBrowser('firefox')) { cy.wait(0).get('@selectionchange').should('be.called') return } cy.wait(10).get('@selectionchange').should('not.be.called') }) it('focus <select>', () => { const $el = cy.$$('<select>') $el.appendTo(cy.$$('body')) $el[0].focus() cy.wrap($el[0]).focus() .should('have.focus') if (Cypress.isBrowser('firefox')) { cy.wait(0).get('@selectionchange').should('be.called') return } cy.wait(10).get('@selectionchange').should('not.be.called') }) it('focus <button>', () => { const $el = cy.$$('<button/>') $el.appendTo(cy.$$('body')) $el[0].focus() cy.wrap($el[0]).focus() .should('have.focus') if (Cypress.isBrowser('firefox')) { cy.wait(0).get('@selectionchange').should('be.called') return } cy.wait(10).get('@selectionchange').should('not.be.called') }) it('focus <iframe>', () => { const $el = cy.$$('<iframe src="" />') $el.appendTo(cy.$$('body')) $el[0].focus() cy.wrap($el[0]).focus() .should('have.focus') cy.wait(0).get('@selectionchange').should('not.be.called') }) it('focus [tabindex]', () => { const $el = cy.$$('<div tabindex="1">tabindex</div>') $el.appendTo(cy.$$('body')) $el[0].focus() if (Cypress.isBrowser('firefox')) { cy.wait(0).get('@selectionchange').should('be.called') return } cy.wait(0).get('@selectionchange').should('not.be.called') }) it('focus <textarea>', () => { const $el = cy.$$('<textarea/>') $el.appendTo(cy.$$('body')) $el[0].focus() cy.wrap($el[0]).focus() .should('have.focus') cy.get('@selectionchange').should('be.called') }) it('focus [contenteditable]', () => { const $el = cy.$$('<div contenteditable>contenteditable</div>') $el.appendTo(cy.$$('body')) $el[0].focus() cy.get('@selectionchange').should('be.called') }) it('cannot focus a [contenteditable] child', () => { const outer = cy.$$('<div contenteditable>contenteditable</div>').appendTo(cy.$$('body')) const inner = cy.$$('<div>first inner contenteditable</div>').appendTo(outer) cy.$$('<div>second inner contenteditable</div>').appendTo(outer) cy.get('input:first').focus() .wait(0) .get('@selectionchange').then((stub) => stub.resetHistory()) cy.wrap(inner).should(($el) => $el.focus) .wait(0) cy.get('input:first').should('have.focus') cy.get('@selectionchange').should('not.be.called') }) it('focus svg', () => { const $svg = cy.$$(`<svg tabindex="1" width="900px" height="500px" viewBox="0 0 95 50" style="border: solid red 1px;" xmlns="http://www.w3.org/2000/svg"> <g data-Name="group" stroke="green" fill="white" stroke-width="5" data-tabindex="0" > <a xlink:href="#"> <circle cx="20" cy="25" r="5" data-Name="shape 1" data-tabindex="0" /> </a> <a xlink:href="#"> <circle cx="40" cy="25" r="5" data-Name="shape 2" data-tabindex="0" /> </a> <a xlink:href="#"> <circle cx="60" cy="25" r="5" data-Name="shape 3" data-tabindex="0" /> </a> <a xlink:href="#"> <circle cx="80" cy="25" r="5" data-Name="shape 4" data-tabindex="0" /> </a> </g> </svg>`).appendTo(cy.$$('body')) cy.wrap($svg).focus().should('have.focus') }) it('focus area', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => { 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" /> `).appendTo(cy.$$('body')) cy.get('area') // make sure the element can receive focus then reset activeElement with blur // without this firefox can fail due to <area> not being ready to receive focus // seems unrelated to 'load' state .should(($el) => { $el.focus() expect($el).be.focused $el.blur() }) // do the actual test now .focus() .should('have.focus') }) }) // W3C Hidden @see html.spec.whatwg.org/multipage/interaction.html#focusable-area // fix https://github.com/cypress-io/cypress/issues/4898 it('does not send focus events for focusable elements that are w3c hidden', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { cy.$$('<input style="visibility:hidden" id="no-focus-1"/>') .appendTo(cy.$$('body')) cy.$$('<input style="display:none" id="no-focus-2"/>') .appendTo(cy.$$('body')) cy.$$('<div style="visibility:hidden"><input id="no-focus-3"/></div>') .appendTo(cy.$$('body')) cy.$$('<div style="display:none"><input id="no-focus-4"/></div>') .appendTo(cy.$$('body')) const stub = cy.stub().as('focus') cy.$$('#no-focus-1').on('focus', stub).get(0).focus() cy.$$('#no-focus-2').on('focus', stub).get(0).focus() cy.$$('#no-focus-3').on('focus', stub).get(0).focus() cy.$$('#no-focus-4').on('focus', stub).get(0).focus() expect(stub).not.called cy.get('#no-focus-1').should('not.be.visible') cy.get('#no-focus-2').should('not.be.visible') cy.get('#no-focus-3').should('not.be.visible') cy.get('#no-focus-4').should('not.be.visible') }) }) // W3C Hidden @see html.spec.whatwg.org/multipage/interaction.html#focusable-area // fix https://github.com/cypress-io/cypress/issues/4898 it('does send focus events for focusable elements that are 0x0 size', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { cy.$$('<input style="width:0;height:0;padding:0;margin:0;border:0;outline:0" id="focus-1"/>') .appendTo(cy.$$('body')) const stub = cy.stub() cy.$$('#focus-1') .on('focus', stub) .get(0).focus() expect(stub).calledOnce cy.get('#focus-1').should('not.be.visible') }) }) })