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

Transform visibility when height/width is set. #6000

Merged
merged 5 commits into from
Jan 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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