UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

300 lines (237 loc) 8.46 kB
import _ from 'lodash' import { getParent } from './elements' import { isDocument } from './document' export const detectVisibility = ($el: any) => { const list = extractTransformInfoFromElements($el) if (existsInvisibleBackface(list)) { return elIsBackface(list) ? 'backface' : 'visible' } return elIsTransformedToZero(list) ? 'transformed' : 'visible' } type BackfaceVisibility = 'hidden' | 'visible' | '' type TransformStyle = 'flat' | 'preserve-3d' type Matrix2D = [ number, number, number, number, number, number, ] type Matrix3D = [ number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, ] type Vector3 = [number, number, number] interface TransformInfo { backfaceVisibility: BackfaceVisibility transformStyle: TransformStyle transform: string } const extractTransformInfoFromElements = ($el: any, list: TransformInfo[] = []): TransformInfo[] => { const info = extractTransformInfo($el) if (info) { list.push(info) } const $parent = getParent($el) if (!$parent.length || isDocument($parent)) { return list } return extractTransformInfoFromElements($parent, list) } const extractTransformInfo = ($el): TransformInfo | null => { const el = $el[0] const style = getComputedStyle(el) const backfaceVisibility = style.getPropertyValue('backface-visibility') as BackfaceVisibility // When an element is not in the DOM tree, getComputedStyle() returns empty string. // In an edge case from frameworks like `vue-fragment` // `parentNode` is modified and out of the DOM tree. // @see https://github.com/cypress-io/cypress/pull/6787 // @see https://github.com/cypress-io/cypress/issues/6745 if (backfaceVisibility === '') { return null } return { backfaceVisibility, transformStyle: style.getPropertyValue('transform-style') as TransformStyle, transform: style.getPropertyValue('transform'), } } const existsInvisibleBackface = (list: TransformInfo[]) => { return !!_.find(list, { backfaceVisibility: 'hidden' }) } const numberRegex = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/g const defaultNormal: Vector3 = [0, 0, 1] const viewVector: Vector3 = [0, 0, -1] const identityMatrix3D: Matrix3D = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, ] // It became 1e-5 from 1e-10. Because 30deg + 30deg + 30deg is 6.0568e-7 and it caused a false negative. const TINY_NUMBER = 1e-5 const nextPreserve3d = (i: number, list: TransformInfo[]) => { return i + 1 < list.length && list[i + 1].transformStyle === 'preserve-3d' } const finalNormal = (startIndex: number, list: TransformInfo[]) => { let i = startIndex let normal = findNormal(parseMatrix3D(list[i].transform)) while (nextPreserve3d(i, list)) { i++ normal = findNormal(parseMatrix3D(list[i].transform), normal) } return normal } const elIsBackface = (list: TransformInfo[]) => { // When the direct parent of the target has style, preserve-3d if (list.length > 1 && list[1].transformStyle === 'preserve-3d') { // When the target is backface-invisible a2-1-1 ~ a2-1-4 if (list[0].backfaceVisibility === 'hidden') { let normal = finalNormal(0, list) if (checkBackface(normal)) { return true } } else { // When the direct parent of the target is backface-invisible if (list[1].backfaceVisibility === 'hidden') { // If it is not none, it is visible. Check a2-3-1 if (list[0].transform === 'none') { let normal = finalNormal(1, list) if (checkBackface(normal)) { return true } } } // Check 90deg a2-2-3, a2-2-4. let normal = finalNormal(0, list) return isElementOrthogonalWithView(normal) } } else { for (let i = 0; i < list.length; i++) { // Ignore preserve-3d when it is not a direct parent. // Why? -> https://github.com/cypress-io/cypress/pull/5916 if (i > 0 && list[i].transformStyle === 'preserve-3d') { continue } if (list[i].backfaceVisibility === 'hidden' && list[i].transform.startsWith('matrix3d')) { let normal = findNormal(parseMatrix3D(list[i].transform)) if (checkBackface(normal)) { return true } } } } return false } // 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 checkBackface = (normal: Vector3) => { // Simplified dot product. // 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 SMALL_NUMBER(10^-10). if (Math.abs(dot) < TINY_NUMBER) { dot = 0 } return dot >= 0 } const parseMatrix3D = (transform: string): Matrix3D => { if (transform === 'none') { return identityMatrix3D } if (transform.startsWith('matrix3d')) { const matrix: Matrix3D = transform.substring(8).match(numberRegex)!.map((n) => { return parseFloat(n) }) as Matrix3D return matrix } return toMatrix3d(transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D) } const parseMatrix2D = (transform: string): Matrix2D => { return transform.match(numberRegex)!.map((n) => parseFloat(n)) as Matrix2D } const findNormal = (matrix: Matrix3D, normal: Vector3 = defaultNormal): Vector3 => { const m = matrix // alias for shorter formula const v = normal // alias for shorter formula const computedNormal: Vector3 = [ 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], ] return toUnitVector(computedNormal) } const toMatrix3d = (m2d: Matrix2D): Matrix3D => { return [ m2d[0], m2d[1], 0, 0, m2d[2], m2d[3], 0, 0, 0, 0, 1, 0, m2d[4], m2d[5], 0, 1, ] } const toUnitVector = (v: Vector3): Vector3 => { const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) 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: TransformInfo[]) => { if (list.some((info) => info.transformStyle === 'preserve-3d')) { const normal = finalNormal(0, list) return isElementOrthogonalWithView(normal) } return !!_.find(list, (info) => isTransformedToZero(info)) } const isTransformedToZero = ({ transform }: TransformInfo) => { 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. // // https://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/ // https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions // if (transform.startsWith('matrix3d')) { const matrix3d = parseMatrix3D(transform) if (is3DMatrixScaledTo0(matrix3d)) { return true } const normal = findNormal(matrix3d) return isElementOrthogonalWithView(normal) } const m = parseMatrix2D(transform) if (is2DMatrixScaledTo0(m)) { return true } return false } const is3DMatrixScaledTo0 = (m3d: Matrix3D) => { 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: Matrix2D) => { 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 = (normal: Vector3) => { // Simplified dot product. // [0] and [1] are always 0 const dot = viewVector[2] * normal[2] return Math.abs(dot) < TINY_NUMBER }