From 018ef38f691fc19d4ffaa7d91585493a0b7cd4f5 Mon Sep 17 00:00:00 2001 From: KHeo Date: Mon, 9 Dec 2019 12:35:02 +0900 Subject: [PATCH 1/9] Moved and added tests to separate file. It's created for visual tests. --- .../test/cypress/fixtures/issue-5682.html | 154 ++++++++++++++++++ .../integration/dom/visibility_spec.js | 61 ------- .../cypress/integration/issues/5682.spec.js | 46 ++++++ 3 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 packages/driver/test/cypress/fixtures/issue-5682.html create mode 100644 packages/driver/test/cypress/integration/issues/5682.spec.js diff --git a/packages/driver/test/cypress/fixtures/issue-5682.html b/packages/driver/test/cypress/fixtures/issue-5682.html new file mode 100644 index 000000000000..83f5b07abd21 --- /dev/null +++ b/packages/driver/test/cypress/fixtures/issue-5682.html @@ -0,0 +1,154 @@ + + + + + + +
+

Basic cases

+ +

No transform

+
No transform
+ +

Rotate < 90deg + backface-invisible

+
rotateX(45deg)
+
rotateY(-45deg)
+ +

Rotate > 90deg + backface-invisible

+
rotateX(135deg)
+
rotateY(-135deg)
+ +

Rotate 90deg + backface-invisible

+
rotateX(90deg)
+
rotateY(-90deg)
+ +

Rotate > 135deg + backface-visible

+
Not hidden + rotateX(150deg)
+
+ +
+

Affected by flat ancestors

+ +
+

CASE 1: All flat

+ +

Parent hidden

+
+
Hidden by parent
+
+ +

Parent Visible

+
+
Not hidden by parent
+
+ +

Parent rotate 180deg + Child backface-invisible

+
+
Parent rotated 180deg
+
+
+ +
+

CASE 2: No transform after preserve-3d

+ +

Parent rotate 180deg + Child backface-invisible

+
+ Parent rotateX(180deg) +
Target
+
+ +

Parent rotate 45deg + Child rotate 45deg with backface-invisible

+
+ Parent rotateX(45deg) +
Target rotateX(180deg)
+
+ +

Parent rotate 90deg + Child rotate 190deg with backface-invisible

+
+ Parent rotateX(90deg) +
Target rotateX(190deg)
+
+ +

Grandparent rotateX 180deg with preserve-3d + Parent rotate 180deg + Child backface-invisible

+
+ Grandparent rotateX(180deg) +
+ Parent rotateX(180deg) +
Target
+
+
+
+ +
+

CASE 3: flat + preserver-3d

+ +

Parent rotate 180deg + Child backface-invisible with flat

+
+ Parent rotateX(180deg) +
Target
+
+ +

Grandparent rotateX 60deg with flat + Parent rotate 60deg + Child backface-invisible

+
+ Grandparent rotateX(60deg) +
+ Parent rotateX(60deg) +
Target
+
+
+ +

Grandparent rotateX 180deg with flat + Parent rotate 180deg + Child backface-invisible

+
+ Grandparent rotateX(180deg) +
+ Parent rotateX(180deg) +
Target
+
+
+ +

Grandgrandparent rotateX 60deg with preserve3d + Grandparent rotateX 60deg with flat + Parent rotate 60deg with preserve3d + Child backface-invisible

+
+ Grandgrandparent rotateX(60deg) +
+ Grandparent rotateX(60deg) +
+ Parent rotateX(60deg) +
Target
+
+
+
+
+
+ + + diff --git a/packages/driver/test/cypress/integration/dom/visibility_spec.js b/packages/driver/test/cypress/integration/dom/visibility_spec.js index a8f9eeb120e4..3422864911aa 100644 --- a/packages/driver/test/cypress/integration/dom/visibility_spec.js +++ b/packages/driver/test/cypress/integration/dom/visibility_spec.js @@ -442,15 +442,6 @@ describe('src/cypress/dom/visibility', () => {
TRANSFORMERS
\ -`) - - this.$parentsWithBackfaceVisibilityHidden = add(`\ -
- front -
-
- back -
\ `) this.$ancestorTransformMakesElOutOfBoundsOfAncestor = add(`\ @@ -916,58 +907,6 @@ describe('src/cypress/dom/visibility', () => { }) }) - describe('css backface-visibility', () => { - describe('element visibility by backface-visibility and rotation', () => { - const add = (el) => { - return $(el).appendTo(cy.$$('body')) - } - - it('is visible when there is no transform', () => { - const el = add('
No transform
') - - expect(el).to.be.visible - }) - - it('is visible when an element is rotated < 90 degrees', () => { - const el = add('
rotateX(45deg)
') - - expect(el).to.be.visible - - const el2 = add('
rotateY(-45deg)
') - - expect(el2).to.be.visible - }) - - it('is invisible when an element is rotated > 90 degrees', () => { - const el = add('
rotateX(135deg)
') - - expect(el).to.be.hidden - - const el2 = add('
rotateY(-135deg)
') - - expect(el2).to.be.hidden - }) - - it('is invisible when an element is rotated in 90 degrees', () => { - const el = add('
rotateX(90deg)
') - - expect(el).to.be.hidden - - const el2 = add('
rotateY(-90deg)
') - - expect(el2).to.be.hidden - }) - }) - - it('is visible when backface not visible', function () { - expect(this.$parentsWithBackfaceVisibilityHidden.find('#front')).to.be.visible - }) - - it('is hidden when backface visible', function () { - expect(this.$parentsWithBackfaceVisibilityHidden.find('#back')).to.be.hidden - }) - }) - describe('#getReasonIsHidden', () => { beforeEach(function () { this.reasonIs = ($el, str) => { diff --git a/packages/driver/test/cypress/integration/issues/5682.spec.js b/packages/driver/test/cypress/integration/issues/5682.spec.js new file mode 100644 index 000000000000..abe408b2bb03 --- /dev/null +++ b/packages/driver/test/cypress/integration/issues/5682.spec.js @@ -0,0 +1,46 @@ +describe('issue #5682 - backface visibility', () => { + beforeEach(() => { + cy.visit('/fixtures/issue-5682.html') + }) + + describe('basic cases', () => { + it('is visible when there is no transform', () => { + cy.get('#b-1').should('be.visible') + }) + + it('is visible when an element is rotated < 90 degrees', () => { + cy.get('#b-2').should('be.visible') + cy.get('#b-3').should('be.visible') + }) + + it('is invisible when an element is rotated > 90 degrees', () => { + cy.get('#b-4').should('not.be.visible') + cy.get('#b-5').should('not.be.visible') + }) + + it('is invisible when an element is rotated in exact 90 degrees', () => { + cy.get('#b-6').should('not.be.visible') + cy.get('#b-7').should('not.be.visible') + }) + + it('is visible when an element is not backface-visibility: hidden but rotated > 90 degrees', () => { + cy.get('#b-8').should('be.visible') + }) + }) + + describe('affected by ancestors', () => { + describe('CASE 1: all transform-style: flat', () => { + it('is invisible when parent is hidden', () => { + cy.get('#a1-1').should('not.be.visible') + }) + + it('is visible when parent is visible', () => { + cy.get('#a1-2').should('be.visible') + }) + + it('is visible when parent is rotated > 90 degrees with an element backface-visibility: hidden', () => { + cy.get('#a1-3').should('be.visible') + }) + }) + }) +}) From ce9d5c3dd652113f2f0ba5ef1cbdd61a91709da9 Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 11 Dec 2019 09:57:12 +0900 Subject: [PATCH 2/9] For future reference. Committed for future reference. --- packages/driver/src/dom/visibility.js | 191 ++++++++++++++---- .../test/cypress/fixtures/issue-5682.html | 121 +++++++++-- .../cypress/integration/issues/5682.spec.js | 36 +++- 3 files changed, 291 insertions(+), 57 deletions(-) diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 6aec80478f8f..f2fb94d81bdd 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -64,11 +64,7 @@ const isHidden = (el, name = 'isHidden()') => { // when an element is scaled to 0 in one axis // it is not visible to users. // So, it is hidden. - if (elIsHiddenByTransform($el)) { - return true - } - - if (elIsBackface($el)) { + if (elIsHiddenByTransform($el) !== 'visible') { return true } @@ -123,52 +119,167 @@ const elHasVisibilityHidden = ($el) => { return $el.css('visibility') === 'hidden' } -const numberRegex = /-?[0-9]+(?:\.[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 elIsHiddenByTransform = ($el) => { + const list = extractTransformInfoFromElements($el) + + if (existsInvisibleBackface(list)) { + let transformList + + if (existsPreserve3d(list)) { + transformList = mergeTransformInfo(list) + } else { + transformList = filterTransformInfo(list) + } + + return elIsBackface(transformList) ? 'backface' : 'visible' + } + + return elIsTransformedToZero(list) ? 'transformed' : 'visible' +} + +const extractTransformInfoFromElements = ($el, list = []) => { + list.push(extractTransformInfo($el)) + + const $parent = $el.parent() + + if (!$parent.length || $document.isDocument($parent)) { + return list + } + + return extractTransformInfoFromElements($parent, list) +} + +const extractTransformInfo = ($el) => { const el = $el[0] const style = getComputedStyle(el) - const backface = style.getPropertyValue('backface-visibility') - const backfaceInvisible = backface === 'hidden' - const transform = style.getPropertyValue('transform') - if (!backfaceInvisible || !transform.startsWith('matrix3d')) { - return false + return { + el, + backfaceVisibility: style.getPropertyValue('backface-visibility'), + transformStyle: style.getPropertyValue('transformStyle'), + transform: style.getPropertyValue('transform'), + } +} + +const existsInvisibleBackface = (list) => { + return !!_.find(list, { backfaceVisibility: 'hidden' }) +} + +const existsPreserve3d = (list) => { + return !!_.find(list, { transformStyle: 'preserve-3d' }) +} + +const mergeTransformInfo = (list) => { + let transformList = [] + + for (let i = 0; i < list.length; i++) { + if (list[i].backfaceVisibility === 'hidden') { + if (list[i].transformStyle === 'preserve-3d') { + const transform = [] + + while (list[i].transformStyle === 'preserve-3d' && + (i + 1 < list.length && list[i + 1].transformStyle === 'flat')) { + transform.push(list[i].transform) + i++ + } + + if (transform.length > 0) { + transformList.push(transform) + } + } + + if (list[i].transform.startsWith('matrix3d')) { + transformList.push([list[i].transform]) + } + } + } + + return transformList +} + +const filterTransformInfo = (list) => { + return list + .filter(({ backfaceVisibility, transform }) => { + return backfaceVisibility === 'hidden' && transform.startsWith('matrix3d') + }) + .map(({ transform }) => [transform]) +} + +const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g +const elIsBackface = (transformList) => { + for (let i = 0; i < transformList.length; i++) { + if (isBackface(transformList[i])) { + return true + } } - const m3d = transform.substring(8).match(numberRegex) - const defaultNormal = [0, 0, -1] - const elNormal = findNormal(m3d) + return false +} + +// This is a simplified version of backface culling. +// https://en.wikipedia.org/wiki/Back-face_culling +// +// We defined view vector, (0, 0, -1), - eye to screen. +// and default normal vector of an element, (0, 0, 1) +// When dot product of them are >= 0, item is visible. +const isBackface = (transform) => { + const viewVector = [0, 0, -1] + const elNormal = findNormal(transform.reverse()) // Simplified dot product. // [0] and [1] are always 0 - const dot = defaultNormal[2] * elNormal[2] + const dot = viewVector[2] * elNormal[2] return dot >= 0 } -const findNormal = (m) => { - const length = Math.sqrt(+m[8] * +m[8] + +m[9] * +m[9] + +m[10] * +m[10]) +const findNormal = (transform, index = 0, normal = [0, 0, 1]) => { + const matrix = transform[index] + + if (!matrix) { + return normal + } + + let m + + if (matrix.startsWith('matrix3d')) { + m = matrix.substring(8).match(numberRegex) + } else { + m = toMatrix3d(matrix.match(numberRegex)) + } + + const v = normal // alias for shorter formula + const computedNormal = [ + m[0] * v[0] + m[4] * v[1] + m[8] * v[2], + m[1] * v[0] + m[5] * v[1] + m[9] * v[2], + m[2] * v[0] + m[6] * v[1] + m[10] * v[2], + ] + + const computedUnitNormal = toUnitVector(computedNormal) - return [+m[8] / length, +m[9] / length, +m[10] / length] + return findNormal(transform, index + 1, computedUnitNormal) } -const elHasVisibilityCollapse = ($el) => { - return $el.css('visibility') === 'collapse' +const toMatrix3d = (m2d) => { + return [ + m2d[0], m2d[1], 0, 0, + m2d[2], m2d[3], 0, 0, + 0, 0, 1, 0, + m2d[4], m2d[5], 0, 1, + ] } -// 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 toUnitVector = (v) => { + const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) - const style = window.getComputedStyle(el) - const transform = style.getPropertyValue('transform') + return [v[0] / length, v[1] / length, v[2] / length] +} + +// This function checks 2 things that can happen: scale and rotate to 0 in width or height. +const elIsTransformedToZero = (list) => { + return !!_.find(list, (info) => isTransformedToZero(info)) +} +const isTransformedToZero = ({ transform, el }) => { // Test scaleZ(0) // width or height of getBoundingClientRect aren't 0 when scaleZ(0). // But it is invisible. @@ -203,6 +314,10 @@ const elIsHiddenByTransform = ($el) => { return false } +const elHasVisibilityCollapse = ($el) => { + return $el.css('visibility') === 'collapse' +} + const elHasDisplayNone = ($el) => { return $el.css('display') === 'none' } @@ -359,14 +474,6 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) { return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl) } - if (elIsHiddenByTransform($parent)) { - return true - } - - if (elIsBackface($parent)) { - return true - } - // continue to recursively walk up the chain until we reach body or html return elIsHiddenByAncestors($parent, $origEl) } @@ -483,7 +590,7 @@ const getReasonIsHidden = function ($el) { return `This element '${node}' is not visible because it has an effective width and height of: '${width} x ${height}' pixels.` } - if (elIsHiddenByTransform($el)) { + if (elIsHiddenByTransform($el) === 'transformed') { return `This element '${node}' is not visible because it is hidden by transform.` } diff --git a/packages/driver/test/cypress/fixtures/issue-5682.html b/packages/driver/test/cypress/fixtures/issue-5682.html index 83f5b07abd21..c436f2eb30ed 100644 --- a/packages/driver/test/cypress/fixtures/issue-5682.html +++ b/packages/driver/test/cypress/fixtures/issue-5682.html @@ -37,32 +37,32 @@

Basic cases

-

No transform

+

b-1: No transform

No transform
-

Rotate < 90deg + backface-invisible

+

b-2, b-3: Rotate < 90deg + backface-invisible

rotateX(45deg)
rotateY(-45deg)
-

Rotate > 90deg + backface-invisible

+

b-4, b-5: Rotate > 90deg + backface-invisible

rotateX(135deg)
rotateY(-135deg)
-

Rotate 90deg + backface-invisible

+

b-6, b-7: Rotate 90deg + backface-invisible

rotateX(90deg)
rotateY(-90deg)
-

Rotate > 135deg + backface-visible

+

b-8: Rotate > 135deg + backface-visible

Not hidden + rotateX(150deg)
-

Affected by flat ancestors

+

Affected by ancestors

CASE 1: All flat

-

Parent hidden

+

Parent Flipped

Hidden by parent
@@ -76,27 +76,114 @@

Parent rotate 180deg + Child backface-invisible

Parent rotated 180deg
+ +

Parent rotate 90deg + Child rotate 190deg with backface-invisible

+
+ Parent rotateX(90deg) +
Target rotateX(190deg)
+

CASE 2: No transform after preserve-3d

+

Parent Flipped + no transform

+
+ Parent rotateX(180deg) +
No transform
+
+ +

Grandparent Flipped + no transform parent + no transform child

+
+ Grandparent rotateX(180deg) +
+ No transform +
No transform
+
+
+ +

Parent Flipped + Identity Transform

+
+ Parent rotateX(180deg) +
Target
+
+ +

Grandparent Flipped + Parent no transform + Identity Transform

+
+ Grandparent rotateX(180deg) +
+ Parent no transform +
Target
+
+
+ +

Grandparent Flipped + Parent identity transform + No Transform

+
+ Grandparent rotateX(180deg) +
+ Parent no transform +
Target
+
+
+ +

Grandparent Flipped + Parent no transform with hidden + Child Indentity

+
+ Grandparent rotateX(180deg) +
+ Parent no transform +
Target
+
+
+ +

Grandgrandparent Flipped + Grandparent no transform + Parent no transform with hidden + Child Indentity

+
+ Grandparent rotateX(180deg) +
+ Grandparent +
+ Parent no transform +
Target
+
+
+
+ +

Grandgrandparent Flipped + Grandparent Identity + Parent no transform with hidden + Child Indentity

+
+ Grandparent rotateX(180deg) +
+ Grandparent +
+ Parent no transform +
Target
+
+
+
+

Parent rotate 180deg + Child backface-invisible

Parent rotateX(180deg) -
Target
+
Target

Parent rotate 45deg + Child rotate 45deg with backface-invisible

Parent rotateX(45deg) -
Target rotateX(180deg)
+
Target rotateX(45deg)
+
+ +

GrandParent 30deg + Parent rotate 30deg + Child 30deg with backface-invisible

+
+ Grandparent rotateX(30deg) +
+ Parent rotateX(30deg) +
Target rotateX(30deg)
+

Parent rotate 90deg + Child rotate 190deg with backface-invisible

Parent rotateX(90deg) -
Target rotateX(190deg)
+
Target rotateX(190deg)

Grandparent rotateX 180deg with preserve-3d + Parent rotate 180deg + Child backface-invisible

@@ -104,7 +191,7 @@

Grandparent rotateX 180deg with preserve-3d + Parent rotate 180deg + Child b Grandparent rotateX(180deg)
Parent rotateX(180deg) -
Target
+
Target

@@ -118,12 +205,18 @@

Parent rotate 180deg + Child backface-invisible with flat

Target
+

Parent BI rotateX 180deg + Child Identidy with flat

+
+ Parent rotateX(180deg) +
Target
+
+

Grandparent rotateX 60deg with flat + Parent rotate 60deg + Child backface-invisible

Grandparent rotateX(60deg)
Parent rotateX(60deg) -
Target
+
Target
@@ -132,7 +225,7 @@

Grandparent rotateX 180deg with flat + Parent rotate 180deg + Child backface Grandparent rotateX(180deg)
Parent rotateX(180deg) -
Target
+
Target
@@ -143,7 +236,7 @@

Grandgrandparent rotateX 60deg with preserve3d + Grandparent rotateX 60deg w Grandparent rotateX(60deg)
Parent rotateX(60deg) -
Target
+
Target
diff --git a/packages/driver/test/cypress/integration/issues/5682.spec.js b/packages/driver/test/cypress/integration/issues/5682.spec.js index abe408b2bb03..f4d988b93069 100644 --- a/packages/driver/test/cypress/integration/issues/5682.spec.js +++ b/packages/driver/test/cypress/integration/issues/5682.spec.js @@ -38,9 +38,43 @@ describe('issue #5682 - backface visibility', () => { cy.get('#a1-2').should('be.visible') }) - it('is visible when parent is rotated > 90 degrees with an element backface-visibility: hidden', () => { + it('is visible when an element is backface-invisible whose parent is rotated > 90deg', () => { cy.get('#a1-3').should('be.visible') }) + + it('is invisible when an element is rotated 190deg whose parent is rotated 90deg', () => { + cy.get('#a1-4').should('not.be.visible') + }) + }) + + describe('CASE 2: No transform after preserve-3d', () => { + it('is invisible when target is not transformed but parent is rotated > 90deg', () => { + cy.get('#a2-1-1').should('not.be.visible') + cy.get('#a2-1-2').should('not.be.visible') + }) + + it('is always visible when target is transformed in identity and visible', () => { + cy.get('#a2-2-1').should('be.visible') + cy.get('#a2-2-2').should('be.visible') + cy.get('#a2-2-3').should('be.visible') + cy.get('#a2-2-4').should('be.visible') + }) + + it('is invisible when an element is backface-invisible whose parent is rotated > 90deg', () => { + cy.get('#a2-5').should('not.be.visible') + }) + + it('is invisible when an element is rotated 45deg and its parent is 45deg', () => { + cy.get('#a2-6').should('not.be.visible') + }) + + it('is visible when target 30deg + parent 30deg + grandparent 30deg', () => { + cy.get('#a2-7').should('be.visible') + }) + + it('is visible when an element is rotated 190deg whose parent is roated 90deg', () => { + cy.get('#a2-8').should('be.visible') + }) }) }) }) From 11a4835f8e2cd8005bb80847c61cf9e15429c16d Mon Sep 17 00:00:00 2001 From: KHeo Date: Wed, 11 Dec 2019 16:18:19 +0900 Subject: [PATCH 3/9] Fixed. --- packages/driver/src/dom/visibility.js | 159 ++++++++----- .../test/cypress/fixtures/issue-5682.html | 218 ++++++++---------- .../issues/{5682.spec.js => 5682_spec.js} | 43 ++-- 3 files changed, 217 insertions(+), 203 deletions(-) rename packages/driver/test/cypress/integration/issues/{5682.spec.js => 5682_spec.js} (63%) diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index f2fb94d81bdd..827f69a2f6d3 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -123,15 +123,7 @@ const elIsHiddenByTransform = ($el) => { const list = extractTransformInfoFromElements($el) if (existsInvisibleBackface(list)) { - let transformList - - if (existsPreserve3d(list)) { - transformList = mergeTransformInfo(list) - } else { - transformList = filterTransformInfo(list) - } - - return elIsBackface(transformList) ? 'backface' : 'visible' + return elIsBackface(list) ? 'backface' : 'visible' } return elIsTransformedToZero(list) ? 'transformed' : 'visible' @@ -156,7 +148,7 @@ const extractTransformInfo = ($el) => { return { el, backfaceVisibility: style.getPropertyValue('backface-visibility'), - transformStyle: style.getPropertyValue('transformStyle'), + transformStyle: style.getPropertyValue('transform-style'), transform: style.getPropertyValue('transform'), } } @@ -165,77 +157,116 @@ const existsInvisibleBackface = (list) => { return !!_.find(list, { backfaceVisibility: 'hidden' }) } -const existsPreserve3d = (list) => { - return !!_.find(list, { transformStyle: 'preserve-3d' }) -} +const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g +const defaultNormal = [0, 0, 1] +// This function uses a simplified version of backface culling. +// https://en.wikipedia.org/wiki/Back-face_culling +// +// We defined view vector, (0, 0, -1), - eye to screen. +// and default normal vector of an element, (0, 0, 1) +// When dot product of them are >= 0, item is visible. +const elIsBackface = (list) => { + const nextPreserve3d = (i) => { + return i + 1 < list.length && + list[i + 1].transformStyle === 'preserve-3d' + } -const mergeTransformInfo = (list) => { - let transformList = [] + let i = 0 - for (let i = 0; i < list.length; i++) { - if (list[i].backfaceVisibility === 'hidden') { - if (list[i].transformStyle === 'preserve-3d') { - const transform = [] + const finalNormal = (startIndex) => { + i = startIndex + let normal = findNormal(list[i].transform) - while (list[i].transformStyle === 'preserve-3d' && - (i + 1 < list.length && list[i + 1].transformStyle === 'flat')) { - transform.push(list[i].transform) - i++ - } + while (nextPreserve3d(i)) { + i++ + normal = findNormal(list[i].transform, normal) + } - if (transform.length > 0) { - transformList.push(transform) - } - } + return normal + } - if (list[i].transform.startsWith('matrix3d')) { - transformList.push([list[i].transform]) - } + const skipToNextFlat = () => { + while (nextPreserve3d(i)) { + i++ } + + i++ } - return transformList -} + // + if (1 < list.length & list[1].transformStyle === 'preserve-3d') { + if (list[0].backfaceVisibility === 'hidden') { + let normal = finalNormal(0) -const filterTransformInfo = (list) => { - return list - .filter(({ backfaceVisibility, transform }) => { - return backfaceVisibility === 'hidden' && transform.startsWith('matrix3d') - }) - .map(({ transform }) => [transform]) -} + if (checkBackface(normal)) { + return true + } -const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g -const elIsBackface = (transformList) => { - for (let i = 0; i < transformList.length; i++) { - if (isBackface(transformList[i])) { - return true + i++ + } else { + if (list[1].backfaceVisibility === 'visible') { + const { width, height } = list[0].el.getBoundingClientRect() + + if (width === 0 || height === 0) { + return true + } + + skipToNextFlat() + } else { + if (list[0].transform !== 'none') { + skipToNextFlat() + } else { + i++ + + let normal = finalNormal(i) + + if (checkBackface(normal)) { + return true + } + + i++ + } + } + } + } else { + for (; i < list.length; i++) { + if (i > 0 && list[i].transformStyle === 'preserve-3d') { + continue + } + + if (list[i].backfaceVisibility === 'hidden' && list[i].transform.startsWith('matrix3d')) { + let normal = findNormal(list[i].transform) + + if (checkBackface(normal)) { + return true + } + } } } return false } -// This is a simplified version of backface culling. -// https://en.wikipedia.org/wiki/Back-face_culling -// -// We defined view vector, (0, 0, -1), - eye to screen. -// and default normal vector of an element, (0, 0, 1) -// When dot product of them are >= 0, item is visible. -const isBackface = (transform) => { +const checkBackface = (normal) => { const viewVector = [0, 0, -1] - const elNormal = findNormal(transform.reverse()) + // Simplified dot product. - // [0] and [1] are always 0 - const dot = viewVector[2] * elNormal[2] + // viewVector[0] and viewVector[1] are always 0. So, they're ignored. + let dot = viewVector[2] * normal[2] + + // Because of the floating point number rounding error, + // cos(90deg) isn't 0. It's 6.12323e-17. + // And it sometimes causes errors when dot product value is something like -6.12323e-17. + // So, we're setting the dot product result to 0 when its absolute value is less than 1e-10(10^-10). + if (Math.abs(dot) < 1e-10) { + dot = 0 + } return dot >= 0 } -const findNormal = (transform, index = 0, normal = [0, 0, 1]) => { - const matrix = transform[index] - - if (!matrix) { +const findNormal = (matrix, normal = defaultNormal) => { + if (matrix === 'none') { return normal } @@ -254,9 +285,7 @@ const findNormal = (transform, index = 0, normal = [0, 0, 1]) => { m[2] * v[0] + m[6] * v[1] + m[10] * v[2], ] - const computedUnitNormal = toUnitVector(computedNormal) - - return findNormal(transform, index + 1, computedUnitNormal) + return toUnitVector(computedNormal) } const toMatrix3d = (m2d) => { @@ -590,11 +619,13 @@ const getReasonIsHidden = function ($el) { return `This element '${node}' is not visible because it has an effective width and height of: '${width} x ${height}' pixels.` } - if (elIsHiddenByTransform($el) === 'transformed') { + const transformResult = elIsHiddenByTransform($el) + + if (transformResult === 'transformed') { return `This element '${node}' is not visible because it is hidden by transform.` } - if (elIsBackface($el)) { + if (transformResult === 'backface') { return `This element '${node}' is not visible because it is rotated and its backface is hidden.` } diff --git a/packages/driver/test/cypress/fixtures/issue-5682.html b/packages/driver/test/cypress/fixtures/issue-5682.html index c436f2eb30ed..99ea3bf0ecee 100644 --- a/packages/driver/test/cypress/fixtures/issue-5682.html +++ b/packages/driver/test/cypress/fixtures/issue-5682.html @@ -31,6 +31,33 @@ width: 200px; height: 60px; } + + .container { + position: relative; + transform-style: preserve-3d; /* This breaks visible check */ + width: 200px; + height: 300px; + } + + .flipped { + transform: rotateY(180deg); + } + + .face { + backface-visibility: hidden; + position: absolute; + width: 100%; + height: 100%; + } + + .front { + background: blue; + } + + .back { + transform: rotateY(180deg); + background: red; + } @@ -85,161 +112,112 @@

Parent rotate 90deg + Child rotate 190deg with backface-invisible

-

CASE 2: No transform after preserve-3d

+

CASE 2: Direct parent preserve-3d elements

-

Parent Flipped + no transform

-
+

Parent Flipped visible + target hidden

+
Parent rotateX(180deg) -
No transform
+
No transform
-

Grandparent Flipped + no transform parent + no transform child

-
- Grandparent rotateX(180deg) -
- No transform -
No transform
+

Parents rotated 60deg each visible + target hidden

+
+ Grandparent rotateX(60deg) +
+ Parent rotateX(60deg) +
No transform
-

Parent Flipped + Identity Transform

-
+

Parent Flipped hidden + target hidden

+
Parent rotateX(180deg) -
Target
+
No transform
-

Grandparent Flipped + Parent no transform + Identity Transform

-
- Grandparent rotateX(180deg) -
- Parent no transform -
Target
-
+

Parent 60deg visible + target hidden 60deg

+
+ Parent rotateX(60deg) +
rotateX(60deg)
-

Grandparent Flipped + Parent identity transform + No Transform

-
- Grandparent rotateX(180deg) -
- Parent no transform -
Target
-
-
- -

Grandparent Flipped + Parent no transform with hidden + Child Indentity

-
- Grandparent rotateX(180deg) -
- Parent no transform -
Target
-
+

Parent Flipped visible + target visible

+
+ Parent rotateX(180deg) +
No transform
-

Grandgrandparent Flipped + Grandparent no transform + Parent no transform with hidden + Child Indentity

-
- Grandparent rotateX(180deg) -
- Grandparent -
- Parent no transform -
Target
-
+

Parents Flipped visible + target visible

+
+ Parent rotateX(60deg) +
+ Parent rotateX(60deg) +
No transform
-

Grandgrandparent Flipped + Grandparent Identity + Parent no transform with hidden + Child Indentity

-
- Grandparent rotateX(180deg) -
- Grandparent -
- Parent no transform -
Target
-
+

Parents Flipped 45deg visible + target visible

+
+ Grandparent rotateX(45deg) +
+ Parent rotateX(45deg) +
No transform
-

Parent rotate 180deg + Child backface-invisible

-
+

Parent Flipped hidden + identity transform

+
Parent rotateX(180deg) -
Target
+
Identity transform
-

Parent rotate 45deg + Child rotate 45deg with backface-invisible

-
- Parent rotateX(45deg) -
Target rotateX(45deg)
+

Parent Flipped hidden + no transform

+
+ Parent rotateX(180deg) +
No transform
-

GrandParent 30deg + Parent rotate 30deg + Child 30deg with backface-invisible

-
- Grandparent rotateX(30deg) -
- Parent rotateX(30deg) -
Target rotateX(30deg)
+

Grandparent rotated 45deg + parent rotated 45deg hidden + target visible

+
+ Grandparent rotateX(45deg) +
+ Parent rotateX(45deg) +
No transform
+
-

Parent rotate 90deg + Child rotate 190deg with backface-invisible

-
- Parent rotateX(90deg) -
Target rotateX(190deg)
-
- -

Grandparent rotateX 180deg with preserve-3d + Parent rotate 180deg + Child backface-invisible

-
- Grandparent rotateX(180deg) -
- Parent rotateX(180deg) -
Target
+
+

CASE 3: Others

+ +

flat after preserve-3d

+
+ Grandgrandparent rotateX(180deg) +
+ Grandparent rotateX(30deg) +
+ Parent rotateX(30deg) +
No transform
+
-
-

CASE 3: flat + preserver-3d

- -

Parent rotate 180deg + Child backface-invisible with flat

-
- Parent rotateX(180deg) -
Target
-
- -

Parent BI rotateX 180deg + Child Identidy with flat

-
- Parent rotateX(180deg) -
Target
-
- -

Grandparent rotateX 60deg with flat + Parent rotate 60deg + Child backface-invisible

-
- Grandparent rotateX(60deg) -
- Parent rotateX(60deg) -
Target
+
+
+ Front
-
- -

Grandparent rotateX 180deg with flat + Parent rotate 180deg + Child backface-invisible

-
- Grandparent rotateX(180deg) -
- Parent rotateX(180deg) -
Target
-
-
- -

Grandgrandparent rotateX 60deg with preserve3d + Grandparent rotateX 60deg with flat + Parent rotate 60deg with preserve3d + Child backface-invisible

-
- Grandgrandparent rotateX(60deg) -
- Grandparent rotateX(60deg) -
- Parent rotateX(60deg) -
Target
-
+
+ Back
+ +
diff --git a/packages/driver/test/cypress/integration/issues/5682.spec.js b/packages/driver/test/cypress/integration/issues/5682_spec.js similarity index 63% rename from packages/driver/test/cypress/integration/issues/5682.spec.js rename to packages/driver/test/cypress/integration/issues/5682_spec.js index f4d988b93069..b0fe6f9cc399 100644 --- a/packages/driver/test/cypress/integration/issues/5682.spec.js +++ b/packages/driver/test/cypress/integration/issues/5682_spec.js @@ -48,33 +48,38 @@ describe('issue #5682 - backface visibility', () => { }) describe('CASE 2: No transform after preserve-3d', () => { - it('is invisible when target is not transformed but parent is rotated > 90deg', () => { - cy.get('#a2-1-1').should('not.be.visible') - cy.get('#a2-1-2').should('not.be.visible') + it('target hidden + parents', () => { + cy.get('#a2-1').should('not.be.visible') + cy.get('#a2-2').should('not.be.visible') + cy.get('#a2-3').should('not.be.visible') + cy.get('#a2-4').should('not.be.visible') }) - it('is always visible when target is transformed in identity and visible', () => { - cy.get('#a2-2-1').should('be.visible') - cy.get('#a2-2-2').should('be.visible') - cy.get('#a2-2-3').should('be.visible') - cy.get('#a2-2-4').should('be.visible') + it('target visible + parent visible', () => { + cy.get('#a2-5').should('be.visible') + cy.get('#a2-6').should('be.visible') + cy.get('#a2-7').should('not.be.visible') }) - it('is invisible when an element is backface-invisible whose parent is rotated > 90deg', () => { - cy.get('#a2-5').should('not.be.visible') - }) - - it('is invisible when an element is rotated 45deg and its parent is 45deg', () => { - cy.get('#a2-6').should('not.be.visible') + it('target visible + parent hidden', () => { + cy.get('#a2-8').should('be.visible') + cy.get('#a2-9').should('not.be.visible') + cy.get('#a2-10').should('not.be.visible') }) + }) - it('is visible when target 30deg + parent 30deg + grandparent 30deg', () => { - cy.get('#a2-7').should('be.visible') + describe('CASE 3: Others', () => { + it('ignores and returns visible when flat appears after preserve-3d', () => { + cy.get('#a3-1').should('be.visible') }) + }) - it('is visible when an element is rotated 190deg whose parent is roated 90deg', () => { - cy.get('#a2-8').should('be.visible') - }) + it('issue case', () => { + cy.get('.front').should('be.visible') + cy.get('.back').should('not.be.visible') + cy.get('.container').click() + cy.get('.front').should('not.be.visible') + cy.get('.back').should('be.visible') }) }) }) From 86f46a83c51713db3ef5a2af87e05da3c4f09264 Mon Sep 17 00:00:00 2001 From: KHeo Date: Fri, 3 Jan 2020 11:45:45 +0900 Subject: [PATCH 4/9] Preparation for merge. --- packages/driver/src/dom/visibility.js | 110 +++++++++++++----- .../integration/dom/visibility_spec.js | 42 ++++++- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 827f69a2f6d3..e19cffdba5fc 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -96,7 +96,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) => { @@ -119,6 +129,12 @@ const elHasVisibilityHidden = ($el) => { return $el.css('visibility') === 'hidden' } +const elComputedStyle = ($el) => { + const el = $el[0] + + return getComputedStyle(el) +} + const elIsHiddenByTransform = ($el) => { const list = extractTransformInfoFromElements($el) @@ -159,6 +175,7 @@ const existsInvisibleBackface = (list) => { const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g const defaultNormal = [0, 0, 1] +const viewVector = [0, 0, -1] // This function uses a simplified version of backface culling. // https://en.wikipedia.org/wiki/Back-face_culling // @@ -175,11 +192,11 @@ const elIsBackface = (list) => { const finalNormal = (startIndex) => { i = startIndex - let normal = findNormal(list[i].transform) + let normal = findNormal(parseMatrix(list[i].transform)) while (nextPreserve3d(i)) { i++ - normal = findNormal(list[i].transform, normal) + normal = findNormal(parseMatrix(list[i].transform), normal) } return normal @@ -235,7 +252,7 @@ const elIsBackface = (list) => { } if (list[i].backfaceVisibility === 'hidden' && list[i].transform.startsWith('matrix3d')) { - let normal = findNormal(list[i].transform) + let normal = findNormal(parseMatrix(list[i].transform)) if (checkBackface(normal)) { return true @@ -248,8 +265,6 @@ const elIsBackface = (list) => { } const checkBackface = (normal) => { - const viewVector = [0, 0, -1] - // Simplified dot product. // viewVector[0] and viewVector[1] are always 0. So, they're ignored. let dot = viewVector[2] * normal[2] @@ -265,19 +280,24 @@ const checkBackface = (normal) => { return dot >= 0 } -const findNormal = (matrix, normal = defaultNormal) => { - if (matrix === 'none') { - return normal +const parseMatrix = (transform) => { + if (transform === 'none') { + return [] } - let m + if (transform.startsWith('matrix3d')) { + return transform.substring(8).match(numberRegex) + } - if (matrix.startsWith('matrix3d')) { - m = matrix.substring(8).match(numberRegex) - } else { - m = toMatrix3d(matrix.match(numberRegex)) + return toMatrix3d(transform.match(numberRegex)) +} + +const findNormal = (matrix, normal = defaultNormal) => { + if (matrix.length === 0) { + return normal } + const m = matrix // alias for shorter formula const v = normal // alias for shorter formula const computedNormal = [ m[0] * v[0] + m[4] * v[1] + m[8] * v[2], @@ -308,13 +328,11 @@ const elIsTransformedToZero = (list) => { return !!_.find(list, (info) => isTransformedToZero(info)) } -const isTransformedToZero = ({ transform, el }) => { - // 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. - // +const isTransformedToZero = ({ transform }) => { + 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. @@ -323,26 +341,56 @@ const isTransformedToZero = ({ transform, el }) => { // https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions // if (transform.startsWith('matrix3d')) { - const m3d = transform.substring(8).match(numberRegex) + const matrix3d = parseMatrix(transform) - // 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 elNormal = findNormal(matrix3d) + // Simplified dot product. + // [0] and [1] are always 0 + const dot = viewVector[2] * elNormal[2] + + return Math.abs(dot) <= 1e-10 +} + const elHasVisibilityCollapse = ($el) => { return $el.css('visibility') === 'collapse' } @@ -659,7 +707,7 @@ ${covered}\ } } - return `Cypress could not determine why this element '${node}' is not visible.` + return `This element '${node}' is not visible.` } /* eslint-enable no-cond-assign */ diff --git a/packages/driver/test/cypress/integration/dom/visibility_spec.js b/packages/driver/test/cypress/integration/dom/visibility_spec.js index 3422864911aa..ff8ee84721a0 100644 --- a/packages/driver/test/cypress/integration/dom/visibility_spec.js +++ b/packages/driver/test/cypress/integration/dom/visibility_spec.js @@ -792,11 +792,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(`
Translated
`) @@ -890,6 +890,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('
Text
') + + expect(el).to.be.visible + }) + + it('is visible when transform is not 0, but width is 0', () => { + const el = add('

Text

') + + expect(el).to.be.visible + }) + + it('is visible when parent transform is not 0, but height is 0', () => { + const el = add('

Text

') + + 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('

Test

') + + 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('

Test

') + + 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 }) @@ -990,7 +1022,7 @@ This element '' is not visible because it has CSS propert }) it('cannot determine why element is not visible', function () { - this.reasonIs(this.$btnOpacity, 'Cypress could not determine why this element \'