const { $, dom } = Cypress

export {}

describe('src/cypress/dom/visibility', () => {
  beforeEach(() => {
    cy.visit('/fixtures/generic.html')
  })

  context('isHidden', () => {
    it('exposes isHidden', () => {
      expect(dom.isHidden).to.be.a('function')
    })

    it('throws when not passed a DOM element', () => {
      const fn = () => {
        dom.isHidden(null!)
      }

      expect(fn).to.throw('`Cypress.dom.isHidden()` failed because it requires a DOM element. The subject received was: `null`')
    })
  })

  context('isVisible', () => {
    it('exposes isVisible', () => {
      expect(dom.isVisible).to.be.a('function')
    })

    it('throws when not passed a DOM element', () => {
      const fn = () => {
        // @ts-ignore
        dom.isVisible('form')
      }

      expect(fn).to.throw('`Cypress.dom.isVisible()` failed because it requires a DOM element. The subject received was: `form`')
    })
  })

  context('#isScrollable', () => {
    beforeEach(function () {
      this.add = (el) => {
        return $(el).appendTo(cy.$$('body'))
      }
    })

    it('returns true if window and body > window height', function () {
      this.add('<div style="height: 1000px; width: 10px;" />')
      const win = cy.state('window')

      const fn = () => {
        return dom.isScrollable(win)
      }

      expect(fn()).to.be.true
    })

    it('returns false window and body > window height', () => {
      cy.$$('body').html('<div>foo</div>')

      const win = cy.state('window')

      const fn = () => {
        return dom.isScrollable(win)
      }

      expect(fn()).to.be.false
    })

    it('returns false el is not scrollable', function () {
      const noScroll = this.add(`\
<div style="height: 100px; overflow: auto;">
  <div>No Scroll</div>
</div>\
`)

      const fn = () => {
        return dom.isScrollable(noScroll)
      }

      expect(fn()).to.be.false
    })

    it('returns false el has no overflow', function () {
      const noOverflow = this.add(`\
<div style="height: 100px; width: 100px; border: 1px solid green;">
  <div style="height: 150px;">
    No Overflow Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Etiam porta sem malesuada magna mollis euismod.
  </div>
</div>\
`)

      const fn = () => {
        return dom.isScrollable(noOverflow)
      }

      expect(fn()).to.be.false
    })

    it('returns true when vertically scrollable', function () {
      const vertScrollable = this.add(`\
<div style="height: 100px; width: 100px; overflow: auto;">
  <div style="height: 150px;">Vertical Scroll</div>
</div>\
`)

      const fn = () => {
        return dom.isScrollable(vertScrollable)
      }

      expect(fn()).to.be.true
    })

    it('returns true when horizontal scrollable', function () {
      const horizScrollable = this.add(`\
<div style="height: 100px; width: 100px; overflow: auto; ">
  <div style="height: 150px;">Horizontal Scroll</div>
</div>\
`)

      const fn = () => {
        return dom.isScrollable(horizScrollable)
      }

      expect(fn()).to.be.true
    })

    it('returns true when overflow scroll forced and content larger', function () {
      const forcedScroll = this.add(`\
<div style="height: 100px; width: 100px; overflow: scroll; border: 1px solid yellow;">
  <div style="height: 300px; width: 300px;">Forced Scroll</div>
</div>\
`)

      const fn = () => {
        return dom.isScrollable(forcedScroll)
      }

      expect(fn()).to.be.true
    })
  })

  context('hidden/visible overrides', () => {
    beforeEach(function () {
      const add = (el) => {
        return $(el).appendTo(cy.$$('body'))
      }

      // ensure all tests run against a scrollable window
      const scrollThisIntoView = add('<div style=`height: 1000px;` /><div>Should be in view</div>')

      this.$visHidden = add('<ul style="visibility: hidden;"></ul>')
      this.$parentVisHidden = add('<div class="invis" style="visibility: hidden;"><button>parent visibility: hidden</button></div>')
      this.$displayNone = add('<button style="display: none">display: none</button>')
      this.$inputHidden = add('<input type="hidden" value="abcdef">')
      this.$divNoWidth = add('<div style="width: 0; height: 100px;">width: 0</div>')
      this.$divNoHeight = add('<div style="width: 50px; height: 0px;">height: 0</div>')
      this.$divDetached = $('<div>foo</div>')
      this.$divVisible = add(`<div>visible</div>`)

      this.$optionInSelect = add(`\
<select>
  <option>Naruto</option>
</select>\
`)

      this.$optgroupInSelect = add(`\
<select>
  <optgroup label='Shinobi'>
    <option>Naruto</option>
  </optgroup>
</select>\
`)

      this.$optionInHiddenSelect = add(`\
<select style='display: none'>
  <option>Sasuke</option>
</select>\
`)

      this.$optionOutsideSelect = add(`\
<div style='display: none'>
  <option id='option-hidden'>Sasuke</option>
</div>
<div>
  <option id='option-visible'>Naruto</option>
</div>\
`)

      this.$optionHiddenInSelect = add(`\
<select>
  <option>--Select--</option>
  <option id="hidden-opt" style='display: none'>Sakura</option>
</select>\
`)

      this.$btnOpacityZero = add(`\
<button style="opacity: 0;">opacity: 0</button>\
`)

      this.$btnOpacityHalf = add(`\
<button style="opacity: 0.5;">opacity: 0.5</button>\
`)

      this.$parentOpacityZero = add(`\
<div style="opacity: 0;">
  <button>parent opacity: 0</button>
</div>\
`)

      this.$tableVisCollapse = add(`\
<table>
  <tr>
    <td>Naruto</td>
    <td class='collapse' style='visibility: collapse;'>Sasuke</td>
    <td>Sakura</td>
  </tr>
  <tr class='collapse' style='visibility: collapse;'>
    <td>Kaguya</td>
    <td><span id='collapse-span'>Madara</span></td>
    <td>Orochimaro</td>
  </tr>
</table>\
`)

      this.$parentNoWidth = add(`\
<div style='width: 0; height: 100px; overflow: hidden;'>
  <div style='height: 500px; width: 500px;'>
    <span>parent width: 0</span>
  </div>
</div>`)

      this.$parentNoHeight = add(`\
<div style='width: 100px; height: 0px; overflow: hidden;'>
  <span>parent height: 0</span>
</div>`)

      this.$parentNoWidthHeightOverflowAuto = add(`\
<div style='width: 0; height: 0px; overflow: auto;'>
  <span>parent no size, overflow: auto</span>
</div>`)

      this.$parentWithWidthHeightNoOverflow = add(`\
<div style='width: 100px; height: 100px; overflow: hidden;'>
  <div style='height: 100px; width: 100px;'>
    <span>parent with size, overflow: hidden</span>
  </div>
</div>`)

      this.$childPosAbs = add(`\
<div style='width: 0; height: 100px; overflow: hidden;'>
  <div style='height: 500px; width: 500px;'>
    <span style='position: absolute;'>position: absolute</span>
  </div>
</div>`)

      this.$childPosFixed = add(`\
<div id="childPosFixed" style='width: 100px; height: 100px; overflow: hidden;'>
  <button style='position: fixed; top: 0;'>position: fixed</button>
</div>`)

      this.$childPointerEventsNone = add(`\
<div style="position: fixed; top: 60px;">
  <span style="pointer-events: none;">child pointer-events: none</span>
</div>\
`)

      this.$descendentPosAbs = add(`\
<div style='width: 0; height: 100px; overflow: hidden;'>
  <div style='height: 500px; width: 500px; position: absolute;'>
    <span>no width, descendant position: absolute</span>
  </div>
</div>`)

      this.$descendentPosFixed = add(`\
<div id="descendentPosFixed" style='width: 0; height: 100px; overflow: hidden;'>
  <div style='height: 500px; width: 500px; position: fixed; top: 0; right: 0;'>
    <button>no width, descendant position: fixed</button>
  </div>
</div>`)

      this.$descendantInPosFixed = add(`\
<div>
  <div id="descendantInPosFixed" style="width: 200px; position: fixed; bottom: 0; right: 0">
    underneath
    <div style="width: 200px; position: fixed; bottom: 0; right: 0">on top of the other</div>
  </div>
</div>\
`)

      this.$coveredUpPosFixed = add(`\
<div>
  <div id="coveredUpPosFixed" style="position: fixed; bottom: 0; left: 0">underneath</div>
  <div style="position: fixed; bottom: 0; left: 0">on top</div>
</div>\
`)

      this.$offScreenPosFixed = add(`\
<div id="offScreenPosFixed" style="position: fixed; bottom: 0; left: -100px;">off screen</div>\
`)

      this.$parentPosAbs = add(`\
<div style='width: 0; height: 100px; overflow: hidden; position: absolute;'>
  <div style='height: 500px; width: 500px;'>
    <span>parent position: absolute</span>
  </div>
</div>`)

      this.$parentDisplayNone = add(`\
<div id="none" style='display: none;'>
  <span>parent display: none</span>
</div>\
`)

      this.$parentPointerEventsNone = add(`\
<div style="pointer-events: none;">
  <span style="position: fixed; top: 20px;">parent pointer-events: none</span>
</div>\
`)

      this.$parentPointerEventsNoneCovered = add(`\
<div style="pointer-events: none;">
  <span style="position: fixed; top: 40px;">parent pointer-events: none</span>
</div>
<span style="position: fixed; top: 40px; background: red;">covering the element with pointer-events: none</span>\
`)

      this.$parentDisplayInlineChildDisplayBlock = add(`\
<div>
  <span>
    <label style="display: block;">display: block</label>
  </span>
</div>\
`)

      this.$elOutOfParentBoundsToLeft = add(`\
<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
  <span style='position: absolute; left: -400px; top: 0px;'>position: absolute, out of bounds left</span>
</div>\
`)

      this.$elOutOfParentBoundsToRight = add(`\
<div id="elOutOfParentBoundsToRight" style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
  <span style='position: absolute; left: 200px; top: 0px;'>position: absolute, out of bounds right</span>
</div>\
`)

      this.$elOutOfParentBoundsAbove = add(`\
<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
  <span style='position: absolute; left: 0px; top: -100px;'>position: absolute, out of bounds above</span>
</div>\
`)

      this.$elOutOfParentBoundsBelow = add(`\
<div id="elOutOfParentBoundsBelow" style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
  <span style='position: absolute; left: 0px; top: 200px;'>position: absolute, out of bounds below</span>
</div>\
`)

      this.$elOutOfParentWithOverflowYHiddenBounds = add(`\
<div style='width: 100px; height: 100px; overflow-y: hidden; position: relative;'>
  <span style='position: absolute; left: 0px; top: 200px;'>position: absolute, out of bounds below</span>
</div>\
`)

      this.$elOutOfParentWithOverflowXHiddenBounds = add(`\
<div style='width: 100px; height: 100px; overflow-x: hidden; position: relative;'>
  <span style='position: absolute; left: 200px; top: 0;'>position: absolute, out of bounds below</span>
</div>\
`)

      this.$elOutOfParentWithFlexAndOverflowHiddenBounds = add(`\
<div style="display: flex; overflow: hidden;">
  <div id="red" style="flex: 0 0 80%; background-color: red;">red</div>
  <div id="green" style="flex: 0 0 80%; background-color: green;">green</div>
  <div id="blue" style="background-color: blue;">blue</div>
</div>\
`)

      this.$elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent = add(`\
<div style="border: 1px solid red; width: 200px; height: 200px; overflow: hidden;">
  <div style="position: absolute; left: 300px; border: 1px solid blue; width: 200px; height: 200px;">
    <span style="border: 1px solid green;">Hello</span>
  </div>\
`)

      this.$elOutOfAncestorOverflowAutoBounds = add(`\
<div style='width: 100px; height: 100px; overflow: auto;'>
  <div style='width: 1000px; position: relative;'>
    <span style='position: absolute; left: 300px; top: 0px;'>out of bounds, parent wide, ancestor overflow: auto</span>
  </div>
</div>\
`)

      this.$elInPosAbsParentsBounds = add(`\
<div style='width: 200px; height: 200px; overflow: hidden; position: relative;'>
  <div style='position: absolute;'>
    <div style='position: absolute;'>
      <span style='position: absolute; left: 50px; top: 50px;'>in bounds, parent position: absolute</span>
    </div>
  </div>
</div>\
`)

      this.$elOutOfPosAbsParentsBounds = add(`\
<div style='width: 100px; height: 100px; overflow: hidden; position: relative; top: 700px; left: 700px;'>
  <div style='position: absolute;'>
    <div style='position: absolute;'>
      <span style='position: absolute; left: -350px; top: 0px;'>out of bounds, parent position: absolute</span>
    </div>
  </div>
</div>\
`)

      this.$elInParentBounds = add(`\
<div style='width: 100px; height: 100px; overflow: hidden; position: relative;'>
  <span style='position: absolute; left: 0px; top: 0px;'>in bounds, position: absolute</span>
</div>\
`)

      this.$elOutOfScrollingParentBounds = add(`\
<div style='width: 100px; height: 100px; overflow: scroll; position: relative; top: 700px; left: 700px;'>
  <div style='position: absolute;'>
    <div style='position: absolute;'>
      <span style='position: absolute; left: 300px; top: 0px;'>out of scrolling bounds, position: absolute</span>
    </div>
  </div>
</div>\
`)

      this.$elIsOutOfBoundsOfAncestorsOverflowButWithinRelativeAncestor = add(`\
<div style='padding: 100px; position: relative;'>
  <div style='overflow: hidden;'>
    <div>
      <span style='position: absolute; left: 0; top: 0;'>in bounds of ancestor, position: absolute, parent overflow: hidden</span>
    </div>
  </div>
</div>\
`)

      this.$elIsRelativeAndOutOfBoundsOfAncestorOverflow = add(`\
<div style='overflow: hidden;'>
  <div>
    <span style='position: relative; left: 0; top: -200px;'>out of bounds, position: relative</span>
  </div>
</div>\
`)

      this.$elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow = add(`\
<div>
  <div>
    <span style='position: relative; left: 0; top: -200px;'>out of bounds but visible, position: relative</span>
  </div>
</div>\
`)

      this.$parentOutOfBoundsButElInBounds = add(`\
<div style='position: relative; padding: 20px;'>
  <div style='overflow: hidden;'>
    <div style='position: relative; left: 0; top: -100px;'>
      <span style='position: relative; left: 0; top: 100px;'>in bounds of ancestor, parent out of bounds</span>
    </div>
  </div>
</div>\
`)

      this.$parentWithClipPathAbsolutePositionElOutsideClipPath = add(`\
<div style="position: absolute; clip-path: polygon(0 0, 0 0, 0 0, 0 0);">
  <span>clip-path</span>
</div>\
`)

      this.$parentWithClipPathAbsolutePositionElInsideClipPath = add(`\
<div style="position: absolute; clip-path: circle(100%);">
  <span>clip-path</span>
</div>\
`)

      this.$parentWithTransformScaleElOutsideScale = add(`\
<div style="transform: scale(0,0)">
  <span>TRANSFORMERS</span>
</div>\
`)

      this.$parentWithTransformScaleElInsideScale = add(`\
<div style="transform: scale(1,1)">
  <span>TRANSFORMERS</span>
</div>\
`)

      this.$ancestorTransformMakesElOutOfBoundsOfAncestor = add(`\
<div style='margin-left: 100px; overflow: hidden; width: 100px;'>
  <div style='transform: translateX(-100px); width: 200px;'>
    <div style='width: 100px;'>
      <span>out of ancestor's bounds due to ancestor translate</span>
    </div>
  </div>
</div>\
`)

      this.$ancestorTransformMakesElInBoundsOfAncestor = add(`\
<div style='margin-left: 100px; overflow: hidden; width: 100px;'>
  <div style='transform: translateX(-100px); width: 300px;'>
    <div style='display: inline-block; width: 100px;'>
      <span>out of ancestor's bounds due to ancestor translate</span>
    </div>
    <div id='inbounds' style='display: inline-block; width: 100px;'>
      <span>in ancestor's bounds due to ancestor translate</span>
    </div>
  </div>
</div>\
`)

      // scroll the 2nd element into view so that
      // there is always a scrollTop so we ensure
      // its factored in (window vs viewport) calculations
      scrollThisIntoView.get(1).scrollIntoView()
    })

    describe('html or body', () => {
      it('is visible if html', () => {
        expect(cy.$$('html').is(':hidden')).to.be.false
        expect(cy.$$('html').is(':visible')).to.be.true

        expect(cy.$$('html')).not.to.be.hidden
        expect(cy.$$('html')).to.be.visible

        cy.wrap(cy.$$('html')).should('not.be.hidden')
        cy.wrap(cy.$$('html')).should('be.visible')
      })

      it('is visible if body', () => {
        expect(cy.$$('body').is(':hidden')).to.be.false
        expect(cy.$$('body').is(':visible')).to.be.true

        expect(cy.$$('body')).not.to.be.hidden
        expect(cy.$$('body')).to.be.visible

        cy.wrap(cy.$$('body')).should('not.be.hidden')
        cy.wrap(cy.$$('body')).should('be.visible')
      })

      it('is visible if display none on body or html', () => {
        cy.$$('html').css('display', 'none')
        cy.$$('body').css('display', 'none')

        expect(cy.$$('html')).not.to.be.hidden
        expect(cy.$$('html')).to.be.visible

        expect(cy.$$('body')).not.to.be.hidden
        expect(cy.$$('body')).to.be.visible
      })
    })

    describe('css visibility', () => {
      it('is hidden if .css(visibility) is hidden', function () {
        expect(this.$visHidden.is(':hidden')).to.be.true
        expect(this.$visHidden.is(':visible')).to.be.false

        expect(this.$visHidden).to.be.hidden
        expect(this.$visHidden).to.not.be.visible

        cy.wrap(this.$visHidden).should('be.hidden')
        cy.wrap(this.$visHidden).should('not.be.visible')
      })

      it('is hidden if parents have .css(visibility) hidden', function () {
        expect(this.$parentVisHidden.find('button').is(':hidden')).to.be.true
        expect(this.$parentVisHidden.find('button').is(':visible')).to.be.false

        expect(this.$parentVisHidden.find('button')).to.be.hidden
        expect(this.$parentVisHidden.find('button')).to.not.be.visible

        cy.wrap(this.$parentVisHidden).find('button').should('be.hidden')
        cy.wrap(this.$parentVisHidden).find('button').should('not.be.visible')
      })

      it('is hidden if visibility collapse', function () {
        expect(this.$tableVisCollapse.find('td.collapse')).to.be.hidden
        expect(this.$tableVisCollapse.find('td.collapse')).to.not.be.visible

        expect(this.$tableVisCollapse.find('tr.collapse')).to.be.hidden
        expect(this.$tableVisCollapse.find('tr.collapse')).to.not.be.visible

        expect(this.$tableVisCollapse.find('tr.collapse td')).to.be.hidden
        expect(this.$tableVisCollapse.find('tr.collapse td')).to.not.be.visible
      })

      it('is hidden if parent has visibility collapse', function () {
        expect(this.$tableVisCollapse.find('tr.collapse td')).to.be.hidden
        expect(this.$tableVisCollapse.find('tr.collapse td')).to.not.be.visible

        expect(this.$tableVisCollapse.find('#collapse-span')).to.be.hidden
        expect(this.$tableVisCollapse.find('#collapse-span')).to.not.be.visible
      })

      it('is hidden if input type hidden', function () {
        expect(this.$inputHidden.is(':hidden')).to.be.true
        expect(this.$inputHidden.is(':visible')).to.be.false

        expect(this.$inputHidden).to.be.hidden
        expect(this.$inputHidden).to.not.be.visible

        cy.wrap(this.$inputHidden).should('be.hidden')
        cy.wrap(this.$inputHidden).should('not.be.visible')
      })
    })

    describe('option and optgroup', () => {
      it('is visible if option in visible select', function () {
        expect(this.$optionInSelect.find('option').is(':hidden')).to.be.false
        expect(this.$optionInSelect.find('option').is(':visible')).to.be.true

        expect(this.$optionInSelect.find('option')).not.to.be.hidden
        expect(this.$optionInSelect.find('option')).to.be.visible

        cy.wrap(this.$optionInSelect.find('option')).should('not.be.hidden')
        cy.wrap(this.$optionInSelect.find('option')).should('be.visible')
      })

      it('is visible if optgroup in visible select', function () {
        expect(this.$optgroupInSelect.find('optgroup').is(':hidden')).to.be.false
        expect(this.$optgroupInSelect.find('optgroup').is(':visible')).to.be.true

        expect(this.$optgroupInSelect.find('optgroup')).not.to.be.hidden
        expect(this.$optgroupInSelect.find('optgroup')).to.be.visible

        cy.wrap(this.$optgroupInSelect.find('optgroup')).should('not.be.hidden')
        cy.wrap(this.$optgroupInSelect.find('optgroup')).should('be.visible')
      })

      it('is hidden if option in hidden select', function () {
        expect(this.$optionInHiddenSelect.find('option').is(':hidden')).to.be.true
        expect(this.$optionInHiddenSelect.find('option').is(':visible')).to.be.false

        expect(this.$optionInHiddenSelect.find('option')).to.be.hidden
        expect(this.$optionInHiddenSelect.find('option')).not.to.be.visible

        cy.wrap(this.$optionInHiddenSelect.find('option')).should('be.hidden')
        cy.wrap(this.$optionInHiddenSelect.find('option')).should('not.be.visible')
      })

      it('is hidden if option is display none', function () {
        expect(this.$optionHiddenInSelect.find('#hidden-opt').is(':hidden')).to.be.true
        expect(this.$optionHiddenInSelect.find('#hidden-opt').is(':visible')).to.be.false

        expect(this.$optionHiddenInSelect.find('#hidden-opt')).to.be.hidden
        expect(this.$optionHiddenInSelect.find('#hidden-opt')).not.to.be.visible

        cy.wrap(this.$optionHiddenInSelect.find('#hidden-opt')).should('be.hidden')
        cy.wrap(this.$optionHiddenInSelect.find('#hidden-opt')).should('not.be.visible')
      })

      it('follows regular visibility logic if option outside of select', function () {
        expect(this.$optionOutsideSelect.find('#option-hidden').is(':hidden')).to.be.true
        expect(this.$optionOutsideSelect.find('#option-hidden')).to.be.hidden
        cy.wrap(this.$optionOutsideSelect.find('#option-hidden')).should('be.hidden')

        expect(this.$optionOutsideSelect.find('#option-visible').is(':visible')).to.be.true
        expect(this.$optionOutsideSelect.find('#option-visible')).to.be.visible

        cy.wrap(this.$optionOutsideSelect.find('#option-visible')).should('be.visible')
      })
    })

    describe('css opacity', () => {
      it('is hidden if opacity is 0', function () {
        expect(this.$btnOpacityZero.is(':hidden')).to.be.true
        expect(this.$btnOpacityZero.is(':visible')).to.be.false

        expect(this.$btnOpacityZero).to.be.hidden
        expect(this.$btnOpacityZero).not.to.be.visible

        cy.wrap(this.$btnOpacityZero).should('be.hidden')
        cy.wrap(this.$btnOpacityZero).should('not.be.visible')
      })

      it('is hidden if parent has `opacity: 0`', function () {
        expect(this.$parentOpacityZero.find('button').is(':hidden')).to.be.true
        expect(this.$parentOpacityZero.find('button').is(':visible')).to.be.false

        expect(this.$parentOpacityZero.find('button')).to.be.hidden
        expect(this.$parentOpacityZero.find('button')).not.to.be.visible

        cy.wrap(this.$parentOpacityZero.find('button')).should('be.hidden')
        cy.wrap(this.$parentOpacityZero.find('button')).should('not.be.visible')
      })

      it('is visible if opacity is greater than 0 but less than 1', function () {
        expect(this.$btnOpacityHalf.is(':visible')).to.be.true
        expect(this.$btnOpacityHalf.is(':hidden')).to.be.false

        expect(this.$btnOpacityHalf).to.be.visible
        expect(this.$btnOpacityHalf).not.to.be.hidden

        cy.wrap(this.$btnOpacityHalf).should('be.visible')
        cy.wrap(this.$btnOpacityHalf).should('not.be.hidden')
      })
    })

    describe('width and height', () => {
      it('is hidden if offsetWidth is 0', function () {
        expect(this.$divNoWidth.is(':hidden')).to.be.true
        expect(this.$divNoWidth.is(':visible')).to.be.false

        expect(this.$divNoWidth).to.be.hidden
        expect(this.$divNoWidth).to.not.be.visible

        cy.wrap(this.$divNoWidth).should('be.hidden')
        cy.wrap(this.$divNoWidth).should('not.be.visible')
      })

      it('is hidden if parent has overflow: hidden and no width', function () {
        expect(this.$parentNoWidth.find('span')).to.be.hidden
        expect(this.$parentNoWidth.find('span')).to.not.be.visible
      })

      it('is hidden if parent has overflow: hidden and no height', function () {
        expect(this.$parentNoHeight.find('span')).to.be.hidden
        expect(this.$parentNoHeight.find('span')).to.not.be.visible
      })

      it('is visible when parent has positive dimensions even with overflow hidden', function () {
        expect(this.$parentWithWidthHeightNoOverflow.find('span')).to.be.visible
        expect(this.$parentWithWidthHeightNoOverflow.find('span')).to.not.be.hidden
      })
    })

    describe('css position', () => {
      it('is visible if child has position: absolute', function () {
        expect(this.$childPosAbs.find('span')).to.be.visible
        expect(this.$childPosAbs.find('span')).not.be.hidden
      })

      it('is visible if child has position: fixed', function () {
        expect(this.$childPosFixed.find('button')).to.be.visible
        expect(this.$childPosFixed.find('button')).not.to.be.hidden
      })

      it('is visible if descendent from parent has position: fixed', function () {
        expect(this.$descendentPosFixed.find('button')).to.be.visible
        expect(this.$descendentPosFixed.find('button')).not.to.be.hidden
      })

      it('is visible if has position: fixed and descendent is found', function () {
        expect(this.$descendantInPosFixed.find('#descendantInPosFixed')).to.be.visible
        expect(this.$descendantInPosFixed.find('#descendantInPosFixed')).not.to.be.hidden
      })

      it('is hidden if position: fixed and covered up', function () {
        expect(this.$coveredUpPosFixed.find('#coveredUpPosFixed')).to.be.hidden
        expect(this.$coveredUpPosFixed.find('#coveredUpPosFixed')).not.to.be.visible
      })

      it('is hidden if position: fixed and off screen', function () {
        expect(this.$offScreenPosFixed).to.be.hidden
        expect(this.$offScreenPosFixed).not.to.be.visible
      })

      it('is visible if descendent from parent has position: absolute', function () {
        expect(this.$descendentPosAbs.find('span')).to.be.visible
        expect(this.$descendentPosAbs.find('span')).to.not.be.hidden
      })

      it('is hidden if only the parent has position absolute', function () {
        expect(this.$parentPosAbs.find('span')).to.be.hidden
        expect(this.$parentPosAbs.find('span')).to.not.be.visible
      })

      it('is visible if position: fixed and parent has pointer-events: none', function () {
        expect(this.$parentPointerEventsNone.find('span')).to.be.visible
        expect(this.$parentPointerEventsNone.find('span')).to.not.be.hidden
      })

      it('is not visible if covered when position: fixed and parent has pointer-events: none', function () {
        expect(this.$parentPointerEventsNoneCovered.find('span')).to.be.hidden
        expect(this.$parentPointerEventsNoneCovered.find('span')).to.not.be.visible
      })

      it('is visible if pointer-events: none and parent has position: fixed', function () {
        expect(this.$childPointerEventsNone.find('span')).to.be.visible
        expect(this.$childPointerEventsNone.find('span')).to.not.be.hidden
      })
    })

    describe('css display', function () {
      // https://github.com/cypress-io/cypress/issues/6183
      it('parent is visible if display inline and child has display block', function () {
        expect(this.$parentDisplayInlineChildDisplayBlock.find('span')).to.be.visible
        expect(this.$parentDisplayInlineChildDisplayBlock.find('span')).to.not.be.hidden
      })
    })

    describe('css overflow', () => {
      it('is visible when parent doesnt have overflow hidden', function () {
        expect(this.$parentNoWidthHeightOverflowAuto.find('span')).to.be.visible

        expect(this.$parentNoWidthHeightOverflowAuto.find('span')).to.not.be.hidden
      })

      it('is hidden when parent overflow hidden and out of bounds to left', function () {
        expect(this.$elOutOfParentBoundsToLeft.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow hidden and out of bounds to right', function () {
        expect(this.$elOutOfParentBoundsToRight.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow hidden and out of bounds above', function () {
        expect(this.$elOutOfParentBoundsAbove.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow hidden and out of bounds below', function () {
        expect(this.$elOutOfParentBoundsBelow.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow-y hidden and out of bounds', function () {
        expect(this.$elOutOfParentWithOverflowYHiddenBounds.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow-x hidden and out of bounds', function () {
        expect(this.$elOutOfParentWithOverflowXHiddenBounds.find('span')).to.be.hidden
      })

      it('is visible when parent overflow hidden but el in a closer parent with position absolute', function () {
        expect(this.$elOutOfParentWithOverflowHiddenBoundsButCloserPositionAbsoluteParent.find('span')).to.be.visible
      })

      it('is hidden when parent flex and overflow hidden and el out of bounds', function () {
        expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#red')).to.be.visible
        expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#green')).to.be.visible
        expect(this.$elOutOfParentWithFlexAndOverflowHiddenBounds.find('#blue')).to.be.hidden
      })

      it('is hidden when parent is wide and ancestor is overflow auto', function () {
        expect(this.$elOutOfAncestorOverflowAutoBounds.find('span')).to.be.hidden
      })

      it('is hidden when parent overflow scroll and out of bounds', function () {
        expect(this.$elOutOfScrollingParentBounds.find('span')).to.be.hidden
      })

      it('is hidden when parent absolutely positioned and overflow hidden and out of bounds', function () {
        expect(this.$elOutOfPosAbsParentsBounds.find('span')).to.be.hidden
      })

      it('is visible when parent absolutely positioned and overflow hidden and not out of bounds', function () {
        expect(this.$elInPosAbsParentsBounds.find('span')).to.be.visible
      })

      it('is visible when parent overflow hidden and not out of bounds', function () {
        expect(this.$elInParentBounds.find('span')).to.be.visible
      })

      it('is visible when ancestor is overflow hidden but more distant ancestor is the offset parent', function () {
        expect(this.$elIsOutOfBoundsOfAncestorsOverflowButWithinRelativeAncestor.find('span')).to.be.visible
      })

      it('is hidden when relatively positioned outside ancestor with overflow hidden', function () {
        expect(this.$elIsRelativeAndOutOfBoundsOfAncestorOverflow.find('span')).to.be.hidden
      })

      it('is visible when el is relatively positioned outside ancestor that does not hide overflow', function () {
        expect(this.$elIsRelativeAndOutOfBoundsOfAncestorButAncestorShowsOverflow.find('span')).to.be.visible
      })

      it('is visible when parent is relatively positioned out of bounds but el is relatively positioned back in bounds', function () {
        expect(this.$parentOutOfBoundsButElInBounds.find('span')).to.be.visible
      })
    })

    describe('css clip-path', () => {
      // TODO: why is this skipped?
      it.skip('is hidden when outside of parents clip-path', function () {
        expect(this.$parentWithClipPathAbsolutePositionElOutsideClipPath.find('span')).to.be.hidden
      })

      it('is visible when inside of parents clip-path', function () {
        expect(this.$parentWithClipPathAbsolutePositionElInsideClipPath.find('span')).to.be.visible
      })
    })

    describe('css transform', () => {
      const add = (el) => {
        return $(el).appendTo(cy.$$('body'))
      }

      describe('element visibility by css transform', () => {
        it('is visible when an element is translated a bit', () => {
          const el = add(`<div style="transform: translate(10px, 10px)">Translated</div>`)

          expect(el).to.be.visible
        })

        it('is visible when an element is only skewed', () => {
          const el = add(`<div style="transform: skew(10deg, 15deg)">Skewed</div>`)

          expect(el).to.be.visible
        })

        it('is visible when an element is only rotated', () => {
          const el = add(`<div style="transform: rotate(20deg)">Rotated</div>`)

          expect(el).to.be.visible
        })

        it('is visible when an element is scaled by non-zero', () => {
          const el = add(`<div style="transform: scale(2, 3)">Scaled</div>`)

          expect(el).to.be.visible
        })

        it('is visible when an element is transformed in multiple ways but not scaled to zero', () => {
          const el = add(`<div style="transform: translate(10px, 15px) skew(30deg) rotate(30deg) scale(4, 1)">Multiple transform</div>`)

          expect(el).to.be.visible
        })

        it('is visible when an element is rotateZ(90deg)', () => {
          const el = add(`<div style="transform: rotateZ(90deg)">rotateZ(90deg)</div>`)

          expect(el).to.be.visible
        })

        // https://github.com/cypress-io/cypress/issues/6745
        it('is visible even if there is a dangling element in the tree', () => {
          cy.visit('/fixtures/dangling-element.html')
          cy.get('.hello')
        })

        it('is hidden when an element is scaled to X axis in 0', () => {
          const el = add(`<div style="transform: scaleX(0)">ScaleX(0)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is scaled to Y axis in 0', () => {
          const el = add(`<div style="transform: scaleY(0)">ScaleY(0)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is scaled to Z axis in 0', () => {
          const el = add(`<div style="transform: scaleZ(0)">ScaleZ(0)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is transformed in multiple ways but scaled to 0 in one axis', () => {
          const el = add(`<div style="transform: translate(15px, 30px) skew(20deg) rotate(40deg) scale(0, 0)">Multiple 2</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is rotateX(90deg)', () => {
          const el = add(`<div style="transform: rotateX(90deg)">rotateX(90deg)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is rotateY(90deg)', () => {
          const el = add(`<div style="transform: rotateX(90deg)">rotateY(90deg)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is rotateX(90deg) rotateY(90deg)', () => {
          const el = add(`<div style="transform: rotateX(90deg) rotateY(90deg)">rotateX(90deg)</div>`)

          expect(el).to.be.hidden
        })

        it('is hidden when an element is transformed in multiple ways but rotated to 90 deg in X or Y axis', () => {
          const el = add(`<div style="transform: rotateX(90deg) skew(30deg, 50deg) translate(15px, 60px) scale(3.5)">rotateX(90deg)</div>`)

          expect(el).to.be.hidden

          const el2 = add(`<div style="transform: rotateY(90deg) skew(30deg, 50deg) translate(15px, 60px) scale(3.5)">rotateX(90deg)</div>`)

          expect(el2).to.be.hidden

          const el3 = add(`<div style="transform: rotateX(90deg) rotateY(90deg) skew(30deg, 50deg) translate(15px, 60px) scale(3.5)">rotateX(90deg)</div>`)

          expect(el3).to.be.hidden
        })
      })

      describe('when height/width is set', () => {
        it('is visible when transform is not 0, but height is 0', () => {
          const el = add('<div style="transform: translate(0, 0); height: 0;">Text</div>')

          expect(el).to.be.visible
        })

        it('is visible when transform is not 0, but width is 0', () => {
          const el = add('<p style="transform: rotateX(30deg); width: 0;">Text</p>')

          expect(el).to.be.visible
        })

        it('is visible when parent transform is not 0, but height is 0', () => {
          const el = add('<div style="transform: translate(0, 0); height: 0;"><p id="tr-p-0">Text</p></div>')

          expect(el.find('#tr-p-0')).to.be.visible
        })

        it('is visible when parent transform is not 0, but width is 0', () => {
          const el = add('<div style="transform: translate(0, 0); height: 0%;"><p id="tr-p-1">Test</p></div>')

          expect(el.find('#tr-p-1')).to.be.visible
        })

        it('is invisible when parent transform is 0, but height is not 0', () => {
          const el = add('<div style="transform: scaleX(0); height: 10px"><p id="tr-p-2">Test</p></div>')

          expect(el.find('#tr-p-2')).to.be.hidden
        })

        describe('invisible when overflow: hidden', () => {
          it('height: 0 + overflow', () => {
            const el = add('<div style="height: 0px; transform: translate(1, 2); overflow: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })

          it('height: 0 + overflow-x', () => {
            const el = add('<div style="height: 0px; transform: translate(1, 2); overflow-x: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })

          it('height: 0 + overflow-y', () => {
            const el = add('<div style="height: 0px; transform: translate(1, 2); overflow-y: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })

          it('width: 0 + overflow', () => {
            const el = add('<div style="width: 0px; transform: translate(1, 2); overflow: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })

          it('width: 0 + overflow-x', () => {
            const el = add('<div style="width: 0px; transform: translate(1, 2); overflow-x: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })

          it('width: 0 + overflow-y', () => {
            const el = add('<div style="width: 0px; transform: translate(1, 2); overflow-y: hidden"><p id="h0th">Test</p></div>')

            expect(el.find('#h0th')).to.be.hidden
          })
        })
      })

      it('is hidden when outside parents transform scale', function () {
        expect(this.$parentWithTransformScaleElOutsideScale.find('span')).to.be.hidden
      })

      it('is visible when inside of parents transform scale', function () {
        expect(this.$parentWithTransformScaleElInsideScale.find('span')).to.be.visible
      })

      it('is hidden when out of ancestor\'s bounds due to ancestor\'s transform', function () {
        expect(this.$ancestorTransformMakesElOutOfBoundsOfAncestor.find('span')).to.be.hidden
      })

      it('is visible when in ancestor\'s bounds due to ancestor\'s transform', function () {
        expect(this.$ancestorTransformMakesElInBoundsOfAncestor.find('#inbounds')).to.be.visible
      })
    })

    describe('#getReasonIsHidden', () => {
      beforeEach(function () {
        this.reasonIs = ($el, str) => {
          expect(dom.getReasonIsHidden($el)).to.eq(str)
        }
      })

      it('has `display: none`', function () {
        this.reasonIs(this.$displayNone, 'This element `<button>` is not visible because it has CSS property: `display: none`')
      })

      it('has a parent with `display: none`', function () {
        this.reasonIs(this.$parentDisplayNone.find('span'), 'This element `<span>` is not visible because its parent `<div#none>` has CSS property: `display: none`')
      })

      it('has `visibility: hidden`', function () {
        this.reasonIs(this.$visHidden, 'This element `<ul>` is not visible because it has CSS property: `visibility: hidden`')
      })

      it('has parent with `visibility: hidden`', function () {
        this.reasonIs(this.$parentVisHidden.find('button'), 'This element `<button>` is not visible because its parent `<div.invis>` has CSS property: `visibility: hidden`')
      })

      it('has `visibility: collapse`', function () {
        this.reasonIs(this.$tableVisCollapse.find('td.collapse'), 'This element `<td.collapse>` is not visible because it has CSS property: `visibility: collapse`')
      })

      it('has parent with `visibility: collapse`', function () {
        this.reasonIs(this.$tableVisCollapse.find('tr.collapse td:first'), 'This element `<td>` is not visible because its parent `<tr.collapse>` has CSS property: `visibility: collapse`')
      })

      it('has `opacity: 0`', function () {
        this.reasonIs(this.$btnOpacityZero, 'This element `<button>` is not visible because it has CSS property: `opacity: 0`')
      })

      it('has parent with `opacity: 0`', function () {
        this.reasonIs(this.$parentOpacityZero.find('button'), 'This element `<button>` is not visible because its parent `<div>` has CSS property: `opacity: 0`')
      })

      it('is detached from the DOM', function () {
        this.reasonIs(this.$divDetached, 'This element `<div>` is not visible because it is detached from the DOM')
      })

      it('has effective zero width', function () {
        this.reasonIs(this.$divNoWidth, 'This element `<div>` is not visible because it has an effective width and height of: `0 x 100` pixels.')
      })

      it('has effective zero height', function () {
        this.reasonIs(this.$divNoHeight, 'This element `<div>` is not visible because it has an effective width and height of: `50 x 0` pixels.')
      })

      it('has a parent with an effective zero width and overflow: hidden', function () {
        this.reasonIs(this.$parentNoHeight.find('span'), 'This element `<span>` is not visible because its parent `<div>` has CSS property: `overflow: hidden` and an effective width and height of: `100 x 0` pixels.')
      })

      it('element sits outside boundaries of parent with overflow clipping', function () {
        this.reasonIs(this.$elOutOfParentBoundsToRight.find('span'), 'This element `<span>` is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: `hidden`, `scroll` or `auto`')
      })

      it('is hidden because it is backface', function () {
        const el = cy.$$('body').append(`<div id="backface-invisible" style="backface-visibility:hidden; transform: rotateX(180deg)">Hello world</div>`)

        this.reasonIs(el.find('#backface-invisible'), `This element \`<div#backface-invisible>\` is not visible because it is rotated and its backface is hidden.`)
      })

      it('is hidden by transform', function () {
        const el = cy.$$('body').append(`<div id="invisible-transform" style="transform: scaleX(0)">Hello world</div>`)

        this.reasonIs(el.find('#invisible-transform'), `This element \`<div#invisible-transform>\` is not visible because it is hidden by transform.`)
      })

      it('element is fixed and being covered', function () {
        this.reasonIs(this.$coveredUpPosFixed.find('#coveredUpPosFixed'), `\
This element \`<div#coveredUpPosFixed>\` is not visible because it has CSS property: \`position: fixed\` and it's being covered by another element:

\`<div style="position: fixed; bottom: 0; left: 0">on top</div>\``)
      })

      it('needs scroll', function () {
        const el = cy.$$('body').append(`
          <div style="position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow-x: hidden; overflow-y: auto;">
            <div style="height: 800px">Big Element</div>
            <button id="needsScroll">MyButton</button>
          </div>
        `)

        this.reasonIs(el.find('#needsScroll'), `This element \`<button#needsScroll>\` is not visible because its ancestor has \`position: fixed\` CSS property and it is overflowed by other elements. How about scrolling to the element with \`cy.scrollIntoView()\`?`)
      })

      it('cannot determine why element is not visible', function () {
        // this element is actually visible
        // but used here as an example that does not match any of the above
        this.reasonIs(this.$divVisible, 'This element `<div>` is not visible.')
      })
    })
  })
})
