Skip to content

Commit

Permalink
Transform visibility when height/width is set. (cypress-io#6000)
Browse files Browse the repository at this point in the history
* Transform visibility when parent height/width is set.

* width: 0 or height: 0 + transform != 'none' => visible.

* Refactor transform checker functions.
  • Loading branch information
sainthkh authored and Keysox committed Jan 7, 2020
1 parent 7ffa3ca commit b6f53cb
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 27 deletions.
87 changes: 64 additions & 23 deletions packages/driver/src/dom/visibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,17 @@ const elHasNoEffectiveWidthOrHeight = ($el) => {
// display:none elements, and generally any elements that are not directly rendered,
// an empty list is returned.

return (elOffsetWidth($el) <= 0) || (elOffsetHeight($el) <= 0) || ($el[0].getClientRects().length <= 0)
// From https://github.com/cypress-io/cypress/issues/5974,
// we learned that when an element has non-'none' transform style value like "translate(0, 0)",
// it is visible even with `height: 0` or `width: 0`.
// That's why we're checking `transform === 'none'` together with elOffsetWidth/Height.

const style = elComputedStyle($el)
const transform = style.getPropertyValue('transform')

return (elOffsetWidth($el) <= 0 && transform === 'none') ||
(elOffsetHeight($el) <= 0 && transform === 'none') ||
($el[0].getClientRects().length <= 0)
}

const elHasNoOffsetWidthOrHeight = ($el) => {
Expand All @@ -123,16 +133,21 @@ const elHasVisibilityHidden = ($el) => {
return $el.css('visibility') === 'hidden'
}

const numberRegex = /-?[0-9]+(?:\.[0-9]+)?/g
const elComputedStyle = ($el) => {
const el = $el[0]

return getComputedStyle(el)
}

const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g
// This is a simplified version of backface culling.
// https://en.wikipedia.org/wiki/Back-face_culling
//
// We defined view normal vector, (0, 0, -1), - eye to screen.
// and default normal vector, (0, 0, 1)
// When dot product of them are >= 0, item is visible.
const elIsBackface = ($el) => {
const el = $el[0]
const style = getComputedStyle(el)
const style = elComputedStyle($el)
const backface = style.getPropertyValue('backface-visibility')
const backfaceInvisible = backface === 'hidden'
const transform = style.getPropertyValue('transform')
Expand Down Expand Up @@ -163,18 +178,13 @@ const elHasVisibilityCollapse = ($el) => {

// This function checks 2 things that can happen: scale and rotate
const elIsHiddenByTransform = ($el) => {
// We need to see the final calculation of the element.
const el = $el[0]

const style = window.getComputedStyle(el)
const style = elComputedStyle($el)
const transform = style.getPropertyValue('transform')

// Test scaleZ(0)
// width or height of getBoundingClientRect aren't 0 when scaleZ(0).
// But it is invisible.
// Experiment -> https://codepen.io/sainthkh/pen/LYYQGpm
// That's why we're checking transfomation matrix here.
//
if (transform === 'none') {
return false
}

// To understand how this part works,
// you need to understand tranformation matrix first.
// Matrix is hard to explain with only text. So, check these articles.
Expand All @@ -183,26 +193,57 @@ const elIsHiddenByTransform = ($el) => {
// https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
//
if (transform.startsWith('matrix3d')) {
const m3d = transform.substring(8).match(numberRegex)
const matrix3d = transform.substring(8).match(numberRegex)

// Z Axis values
if (+m3d[2] === 0 && +m3d[6] === 0 && +m3d[10] === 0) {
if (is3DMatrixScaledTo0(matrix3d)) {
return true
}

return isElementOrthogonalWithView(matrix3d)
}

// Other cases
if (transform !== 'none') {
const { width, height } = el.getBoundingClientRect()
const m = transform.match(numberRegex)

if (width === 0 || height === 0) {
return true
}
if (is2DMatrixScaledTo0(m)) {
return true
}

return false
}

const is3DMatrixScaledTo0 = (m3d) => {
const xAxisScaledTo0 = +m3d[0] === 0 && +m3d[4] === 0 && +m3d[8] === 0
const yAxisScaledTo0 = +m3d[1] === 0 && +m3d[5] === 0 && +m3d[9] === 0
const zAxisScaledTo0 = +m3d[2] === 0 && +m3d[6] === 0 && +m3d[10] === 0

if (xAxisScaledTo0 || yAxisScaledTo0 || zAxisScaledTo0) {
return true
}

return false
}

const is2DMatrixScaledTo0 = (m) => {
const xAxisScaledTo0 = +m[0] === 0 && +m[2] === 0
const yAxisScaledTo0 = +m[1] === 0 && +m[3] === 0

if (xAxisScaledTo0 || yAxisScaledTo0) {
return true
}

return false
}

const isElementOrthogonalWithView = (matrix3d) => {
const defaultNormal = [0, 0, -1]
const elNormal = findNormal(matrix3d)
// Simplified dot product.
// [0] and [1] are always 0
const dot = defaultNormal[2] * elNormal[2]

return Math.abs(dot) <= 1e-10
}

const elHasDisplayNone = ($el) => {
return $el.css('display') === 'none'
}
Expand Down
40 changes: 36 additions & 4 deletions packages/driver/test/cypress/integration/dom/visibility_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -801,11 +801,11 @@ describe('src/cypress/dom/visibility', () => {
})

describe('css transform', () => {
describe('element visibility by css transform', () => {
const add = (el) => {
return $(el).appendTo(cy.$$('body'))
}
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>`)

Expand Down Expand Up @@ -899,6 +899,38 @@ describe('src/cypress/dom/visibility', () => {
})
})

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
})
})

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

0 comments on commit b6f53cb

Please sign in to comment.