From 8ff44bd4c78788bf76d74c937f3a53ef6df23d1c Mon Sep 17 00:00:00 2001 From: jkodu Date: Wed, 11 Dec 2019 14:34:39 +0000 Subject: [PATCH 1/5] fix: compute orientation lock from transformation functions that affect rotation along z-axis --- lib/checks/mobile/css-orientation-lock.js | 314 +++++++++++------ test/checks/mobile/css-orientation-lock.js | 386 +++++++++++---------- 2 files changed, 410 insertions(+), 290 deletions(-) diff --git a/lib/checks/mobile/css-orientation-lock.js b/lib/checks/mobile/css-orientation-lock.js index 60d92c5f54..0e92cd3f07 100644 --- a/lib/checks/mobile/css-orientation-lock.js +++ b/lib/checks/mobile/css-orientation-lock.js @@ -1,134 +1,250 @@ /* global context */ - -// extract asset of type `cssom` from context const { cssom = undefined } = context || {}; - -// if there is no cssom <- return incomplete if (!cssom || !cssom.length) { return undefined; } -// combine all rules from each sheet into one array -const rulesGroupByDocumentFragment = cssom.reduce( - (out, { sheet, root, shadowId }) => { - // construct key based on shadowId or top level document +let isLocked = false; +let relatedElements = []; +const rulesGroupByDocumentFragment = groupCssomByDocument(cssom); + +for (const { root, rules } of Object.values(rulesGroupByDocumentFragment)) { + const orientationRules = getOrientaionCssMediaFeatureRules(rules); + if (!orientationRules.length) { + continue; + } + + orientationRules.forEach(({ cssRules }) => { + Array.from(cssRules).forEach(cssRule => { + const locked = getIsOrientationLocked(cssRule); + + // if locked and not root HTML, preserve as relatedNodes + if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') { + const elms = + Array.from(root.querySelectorAll(cssRule.selectorText)) || []; + relatedElements = relatedElements.concat(elms); + } + + // set locked boolean + isLocked = locked; + }); + }); +} + +if (!isLocked) { + return true; +} +if (relatedElements.length) { + this.relatedNodes(relatedElements); +} +return false; + +/** + * Group given cssom by document/ document fragment + * @param {Array} allCssom cssom + * @return {Object} + */ +function groupCssomByDocument(cssObjectModel) { + return cssObjectModel.reduce((out, { sheet, root, shadowId }) => { const key = shadowId ? shadowId : 'topDocument'; - // init property if does not exist + if (!out[key]) { - out[key] = { - root, - rules: [] - }; + out[key] = { root, rules: [] }; } - // check if sheet and rules exist + if (!sheet || !sheet.cssRules) { - //return return out; } + const rules = Array.from(sheet.cssRules); - // add rules into same document fragment out[key].rules = out[key].rules.concat(rules); - //return return out; - }, - {} -); + }, {}); +} -// Note: -// Some of these functions can be extracted to utils, but best to do it when other cssom rules are authored. +/** + * Get CSS Rules that target Orientation CSS Media Features + * @param {Array} cssRules + * @returns {Array} + */ +function getOrientaionCssMediaFeatureRules(cssRules) { + return ( + cssRules + /** + * Filter: + * CSSRule.MEDIA_Rule + * -> https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule + */ + .filter(({ type }) => type === 4) + /** + * Filter: + * CSSRule with conditionText of `orientation` + */ + .filter(({ cssText }) => { + return ( + /orientation:\s*landscape/i.test(cssText) || + /orientation:\s*portrait/i.test(cssText) + ); + }) + /** + * Filter: + * Ensure RULELIST is not empty + */ + .filter(({ cssRules }) => cssRules.length > 0) + ); +} -// extract styles for each orientation rule to verify transform is applied -let isLocked = false; -let relatedElements = []; +/** + * Interpolate a given CSS Rule to ascertain if orientation is locked by use of any transformation functions that affect rotation along the Z Axis + * @param {Object} cssRule given CSS Rule + * @property {String} cssRule.selectorText selector text targetted by given cssRule + * @property {Object} cssRule.style style + * @return {Boolean} + */ +function getIsOrientationLocked({ selectorText, style }) { + if (!selectorText || style.length <= 0) { + return false; + } -Object.keys(rulesGroupByDocumentFragment).forEach(key => { - const { root, rules } = rulesGroupByDocumentFragment[key]; + const transformStyle = + style.transform || style.webkitTransform || style.msTransform || false; + if (!transformStyle) { + return false; + } - // filter media rules from all rules - const mediaRules = rules.filter(r => { - // doc: https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule - // type value of 4 (CSSRule.MEDIA_RULE) pertains to media rules - return r.type === 4; - }); - if (!mediaRules || !mediaRules.length) { - return; + /** + * get last match/occurence of a transformation function that can affect rotation along Z axis + */ + const matches = transformStyle.match( + /(rotate|rotateZ|rotate3d|matrix|matrix3d)\(([^)]+)\)(?!.*(rotate|rotateZ|rotate3d|matrix|matrix3d))/ + ); + if (!matches) { + return false; } - // narrow down to media rules with `orientation` as a keyword - const orientationRules = mediaRules.filter(r => { - // conditionText exists on media rules, which contains only the @media condition - // eg: screen and (max-width: 767px) and (min-width: 320px) and (orientation: landscape) - const cssText = r.cssText; - return ( - /orientation:\s*landscape/i.test(cssText) || - /orientation:\s*portrait/i.test(cssText) - ); - }); - if (!orientationRules || !orientationRules.length) { - return; + const [, transformFn, transformFnValue] = matches; + const degrees = getRotationInDegrees(transformFn, transformFnValue); + if (!degrees) { + return false; } - orientationRules.forEach(r => { - // r.cssRules is a RULEList and not an array - if (!r.cssRules.length) { - return; - } - // cssRules ia a list of rules - // a media query has framents of css styles applied to various selectors - // iteration through cssRules and see if orientation lock has been applied - Array.from(r.cssRules).forEach(cssRule => { - // ensure selectorText exists - if (!cssRule.selectorText) { - return; - } - // ensure the given selector has styles declared (non empty selector) - if (cssRule.style.length <= 0) { - return; - } + const locked = degrees % 90 === 0 && degrees % 180 !== 0; + return !!locked; +} - // check if transform style exists (don't forget vendor prefixes) - const transformStyleValue = - cssRule.style.transform || - cssRule.style.webkitTransform || - cssRule.style.msTransform || - false; - // transformStyleValue -> is the value applied to property - // eg: "rotate(-90deg)" - if (!transformStyleValue) { +/** + * Interpolate rotation along the z axis from a given value to a transform function + * @param {String} transformFunction CSS transformation function + * @param {String} transformFnValue value applied to a transform function (contains a unit) + * @returns {Number} + */ +function getRotationInDegrees(transformFunction, transformFnValue) { + switch (transformFunction) { + case 'rotate': + case 'rotateZ': + return getAngleInDegrees(transformFnValue); + case 'rotate3d': + const [, , z, angleWithUnit] = transformFnValue + .split(',') + .map(value => value.trim()); + if (z === 0) { + // no transform is applied along z axis -> ignore return; } + return getAngleInDegrees(angleWithUnit); + case 'matrix': + case 'matrix3d': + return getAngleInDegreesFromMatrixTransform(transformFnValue); + default: + return; + } +} - const rotate = transformStyleValue.match(/rotate\(([^)]+)deg\)/); - const deg = parseInt((rotate && rotate[1]) || 0); - const locked = deg % 90 === 0 && deg % 180 !== 0; +/** + * Get angle in degrees from a transform value by interpolating the unit of measure + * @param {String} angleWithUnit value applied to a transform function (Eg: 1turn) + * @returns{Number|undefined} + */ +function getAngleInDegrees(angleWithUnit) { + const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/); + if (!unit) { + return; + } - // if locked - // and not root HTML - // preserve as relatedNodes - if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') { - const selector = cssRule.selectorText; - const elms = Array.from(root.querySelectorAll(selector)); - if (elms && elms.length) { - relatedElements = relatedElements.concat(elms); - } - } + const angle = parseFloat(angleWithUnit.replace(unit, ``)); + switch (unit) { + case 'rad': + return convertRadToDeg(angle); + case 'grad': + return convertGradToDeg(angle); + case 'turn': + return convertTurnToDeg(angle); + case 'deg': + default: + return parseInt(angle); + } +} - // set locked boolean - isLocked = locked; - }); - }); -}); +/** + * Get angle in degrees from a transform value applied to `matrix` or `matrix3d` transform functions + * @param {String} transformFnValue value applied to a transform function (contains a unit) + * @returns {Number} + */ +function getAngleInDegreesFromMatrixTransform(transformFnValue) { + const values = transformFnValue.split(','); + + /** + * Matrix 2D + * Notes: https://css-tricks.com/get-value-of-css-rotation-through-javascript/ + */ + if (values.length <= 6) { + const [a, b] = values; + const radians = Math.atan2(parseFloat(b), parseFloat(a)); + return convertRadToDeg(radians); + } -if (!isLocked) { - // return - return true; + /** + * Matrix 3D + * Notes: https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix + */ + const sinB = parseFloat(values[8]); + const b = Math.asin(sinB); + const cosB = Math.cos(b); + const rotateZRadians = Math.acos(parseFloat(values[0]) / cosB); + return convertRadToDeg(rotateZRadians); } -// set relatedNodes -if (relatedElements.length) { - this.relatedNodes(relatedElements); +/** + * Convert angle specified in unit radians to degrees + * See - https://drafts.csswg.org/css-values-3/#rad + * @param {Number} radians radians + * @return {Number} + */ +function convertRadToDeg(radians) { + return parseInt(radians * (180 / Math.PI)); } -// return fail -return false; +/** + * Convert angle specified in unit grad to degrees + * See - https://drafts.csswg.org/css-values-3/#grad + * @param {Number} grad grad + * @return {Number} + */ +function convertGradToDeg(grad) { + grad = grad % 400; + if (grad < 0) { + grad += 400; + } + return parseInt((grad / 400) * 360); +} + +/** + * Convert angle specifed in unit turn to degrees + * See - https://drafts.csswg.org/css-values-3/#turn + * @param {Number} turn + * @returns {Number} + */ +function convertTurnToDeg(turn) { + return parseInt(360 / (1 / turn)); +} diff --git a/test/checks/mobile/css-orientation-lock.js b/test/checks/mobile/css-orientation-lock.js index 7d66892d31..bea8a6abad 100644 --- a/test/checks/mobile/css-orientation-lock.js +++ b/test/checks/mobile/css-orientation-lock.js @@ -2,32 +2,15 @@ describe('css-orientation-lock tests', function() { 'use strict'; var checkContext = axe.testUtils.MockCheckContext(); - var origCheck = checks['css-orientation-lock']; + var check = checks['css-orientation-lock']; var dynamicDoc = document.implementation.createHTMLDocument( 'Dynamic document for CSS Orientation Lock tests' ); afterEach(function() { - checks['css-orientation-lock'] = origCheck; checkContext.reset(); }); - var SHEET_DATA = { - BODY_STYLE: 'body { color: inherit; }', - MEDIA_STYLE_NON_ORIENTATION: - '@media (min-width: 400px) { background-color: red; }', - MEDIA_STYLE_ORIENTATION_EMPTY: - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { }', - MEDIA_STYLE_ORIENTATION_WITHOUT_TRANSFORM: - '@media screen and (min-width: 1px) and (max-width: 2000px) and (orientation: portrait) { #mocha { color: red; } }', - MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_NOT_ROTATE: - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: translateX(10px); -webkit-transform: translateX(10px); } }', - MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_ROTATE_180: - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: rotate(180deg); -webkit-transform: rotate(180deg); } }', - MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_ROTATE_90: - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate(270deg); -webkit-transform: rotate(270deg); } }' - }; - function getSheet(data) { var style = dynamicDoc.createElement('style'); style.type = 'text/css'; @@ -36,206 +19,227 @@ describe('css-orientation-lock tests', function() { return style.sheet; } - it('ensure that the check "css-orientation-lock" is invoked', function() { - checks['css-orientation-lock'] = { - evaluate: function() { - return 'invoked'; - } - }; - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document - ); - assert.equal(actual, 'invoked'); - }); - - it('returns undefined if context of check does not have CSSOM property', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document - ); + it('returns undefined when CSSOM is undefined', function() { + var actual = check.evaluate.call(checkContext, document); assert.isUndefined(actual); }); - it('returns undefined if CSSOM does not have any sheets', function() { - // pass context with cssom as empty - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [] - } - ); + it('returns undefined when CSSOM is empty', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [] // pass context with cssom as empty + }); assert.isUndefined(actual); }); - it('returns true if CSSOM does not have sheet or rule(s) in the sheet(s)', function() { - // pass context with cssom but empty or no sheet - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: 'a', - sheet: {} // empty sheet - }, - { - shadowId: 'a' - // NO SHEET -> this should never happen, but testing for iteration exit in check - } - ] - } - ); + it('returns true when CSSOM has no rules', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + sheet: {} // empty sheet, no css rules + } + ] + }); + assert.isTrue(actual); + }); + + it('returns true when CSSOM has no CSS media features', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + sheet: getSheet('body { color: inherit; }') + } + ] + }); + assert.isTrue(actual); + }); + + it('returns true when CSSOM has no CSS media features targeting orientation', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + sheet: getSheet('body { color: inherit; }') + }, + { + shadowId: 'a', + sheet: getSheet( + '@media (min-width: 400px) { background-color: red; }' + ) + } + ] + }); + assert.isTrue(actual); + }); + + it('returns true when CSSOM has empty Orientation CSS media features', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + sheet: getSheet('body { color: inherit; }') + }, + { + shadowId: 'a', + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if there are no MEDIA rule(s) in the CSSOM stylesheets', function() { - var sheet = getSheet(SHEET_DATA.BODY_STYLE); - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: 'a', - sheet: sheet - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features that does not target transform property', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 2000px) and (orientation: portrait) { #mocha { color: red; } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if there are no ORIENTATION rule(s) within MEDIA rules in CSSOM stylesheets', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: undefined, - sheet: getSheet(SHEET_DATA.BODY_STYLE) - }, - { - shadowId: 'a', - sheet: getSheet(SHEET_DATA.MEDIA_STYLE_NON_ORIENTATION) - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features with transform property and transformation function of translateX, which does not affect rotation', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: translateX(10px); -webkit-transform: translateX(10px); } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if no styles within any of the ORIENTATION rule(s)', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: undefined, - sheet: getSheet(SHEET_DATA.BODY_STYLE) - }, - { - shadowId: 'a', - sheet: getSheet(SHEET_DATA.MEDIA_STYLE_ORIENTATION_EMPTY) - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of rotate, which affects rotation but does not lock orientation (rotate(180deg))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: rotate(180deg); -webkit-transform: rotate(180deg); } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if there is no TRANSFORM style within any of the ORIENTATION rule(s)', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: 'a', - sheet: getSheet( - SHEET_DATA.MEDIA_STYLE_ORIENTATION_WITHOUT_TRANSFORM - ) - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of rotateZ, which affects rotation but does not lock orientation (rotateZ(1turn))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: rotateZ(1turn); } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if TRANSFORM style applied is not ROTATE', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: undefined, - sheet: getSheet( - SHEET_DATA.MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_NOT_ROTATE - ) - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of rotate3d, which affects rotation but does not lock orientation (rotate3d(0,0,0,400grad))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + // Note: values set on rotate3d cascasdes over rotate + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: rotate(90deg) rotate3d(0,0,1, 400grad); } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns true if TRANSFORM style applied is ROTATE, but is divisible by 180', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: 'a', - root: document, - sheet: getSheet( - SHEET_DATA.MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_ROTATE_180 - ) - } - ] - } - ); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of matrix3d, which affects rotation but does not lock orientation (matrix3d(-1,0,0.00,0,0.00,-1,0.00,0,0,0,1,0,0,0,0,1))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + // Here the target is rotated by 180deg + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: matrix3d(-1,0,0.00,0,0.00,-1,0.00,0,0,0,1,0,0,0,0,1); } }' + ) + } + ] + }); assert.isTrue(actual); }); - it('returns false if TRANSFORM style applied is ROTATE, and is divisible by 90 and not divisible by 180', function() { - var actual = checks['css-orientation-lock'].evaluate.call( - checkContext, - document, - {}, - undefined, - { - cssom: [ - { - shadowId: undefined, - root: document, - sheet: getSheet( - SHEET_DATA.MEDIA_STYLE_ORIENTATION_WITH_TRANSFORM_ROTATE_90 - ) - } - ] - } - ); + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate, which affects rotation and locks orientation (rotate(270deg))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate(270deg); -webkit-transform: rotate(270deg); } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate3d, which affects rotation and locks orientation (rotate3d(0,0,1,1.5708rad))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,1.5708rad) } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of matrix, which affects rotation and locks orientation (matrix(0.00,1.00,-1.00,0.00,0,0))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + // this rotates by 90deg + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform:matrix(0.00,1.00,-1.00,0.00,0,0); } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of matrix3d, which affects rotation and locks orientation (matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1);)', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + // this rotates by 90deg + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1); } }' + ) + } + ] + }); assert.isFalse(actual); }); From 30c45b7759b883b71d4e3e6babbf0d17826745ea Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 12 Dec 2019 15:17:21 +0000 Subject: [PATCH 2/5] test: add more unit and integration tests --- lib/checks/mobile/css-orientation-lock.js | 5 +++-- test/checks/mobile/css-orientation-lock.js | 16 ++++++++++++++++ .../full/css-orientation-lock/violations.css | 4 ++-- .../full/css-orientation-lock/violations.js | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/checks/mobile/css-orientation-lock.js b/lib/checks/mobile/css-orientation-lock.js index 0e92cd3f07..6c8882fb5c 100644 --- a/lib/checks/mobile/css-orientation-lock.js +++ b/lib/checks/mobile/css-orientation-lock.js @@ -8,7 +8,8 @@ let isLocked = false; let relatedElements = []; const rulesGroupByDocumentFragment = groupCssomByDocument(cssom); -for (const { root, rules } of Object.values(rulesGroupByDocumentFragment)) { +for (const key of Object.keys(rulesGroupByDocumentFragment)) { + const { root, rules } = rulesGroupByDocumentFragment[key]; const orientationRules = getOrientaionCssMediaFeatureRules(rules); if (!orientationRules.length) { continue; @@ -148,7 +149,7 @@ function getRotationInDegrees(transformFunction, transformFnValue) { const [, , z, angleWithUnit] = transformFnValue .split(',') .map(value => value.trim()); - if (z === 0) { + if (parseInt(z) === 0) { // no transform is applied along z axis -> ignore return; } diff --git a/test/checks/mobile/css-orientation-lock.js b/test/checks/mobile/css-orientation-lock.js index bea8a6abad..297a247d5a 100644 --- a/test/checks/mobile/css-orientation-lock.js +++ b/test/checks/mobile/css-orientation-lock.js @@ -196,6 +196,22 @@ describe('css-orientation-lock tests', function() { assert.isFalse(actual); }); + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate3d, which affects rotation and locks orientation (rotate3d(0,0,1,90deg))', function() { + var actual = check.evaluate.call(checkContext, document, {}, undefined, { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + // apply 0 on the z-axis (3rd parameter) does not apply given rotation on z-axis + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,90deg) } }' + ) + } + ] + }); + assert.isFalse(actual); + }); + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate3d, which affects rotation and locks orientation (rotate3d(0,0,1,1.5708rad))', function() { var actual = check.evaluate.call(checkContext, document, {}, undefined, { cssom: [ diff --git a/test/integration/full/css-orientation-lock/violations.css b/test/integration/full/css-orientation-lock/violations.css index f4c5c45e9d..48556c6147 100644 --- a/test/integration/full/css-orientation-lock/violations.css +++ b/test/integration/full/css-orientation-lock/violations.css @@ -6,9 +6,9 @@ @media screen and (min-width: 10px) and (max-width: 3000px) and (orientation: landscape) { html { - transform: rotate(-90deg); + transform: rotateZ(0, 0, 1, 1.5708rad); } .someDiv { - transform: rotate(90deg); + transform: matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1); } } diff --git a/test/integration/full/css-orientation-lock/violations.js b/test/integration/full/css-orientation-lock/violations.js index ce94d605fe..327e1c041b 100644 --- a/test/integration/full/css-orientation-lock/violations.js +++ b/test/integration/full/css-orientation-lock/violations.js @@ -82,7 +82,7 @@ describe('css-orientation-lock violations test', function() { var fixture = document.getElementById('shadow-fixture'); var shadow = fixture.attachShadow({ mode: 'open' }); shadow.innerHTML = - '' + + '' + '
green
' + '
red
'; From 272d4b1c8979a469bfddf7eaf61ae7fe781ee87c Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 12 Dec 2019 15:29:29 +0000 Subject: [PATCH 3/5] test: update transform with browser prefixes --- test/checks/mobile/css-orientation-lock.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/checks/mobile/css-orientation-lock.js b/test/checks/mobile/css-orientation-lock.js index 297a247d5a..3ee423e72a 100644 --- a/test/checks/mobile/css-orientation-lock.js +++ b/test/checks/mobile/css-orientation-lock.js @@ -204,7 +204,7 @@ describe('css-orientation-lock tests', function() { root: document, sheet: getSheet( // apply 0 on the z-axis (3rd parameter) does not apply given rotation on z-axis - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,90deg) } }' + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,90deg); -webkit-transform: rotate3d(0,0,1,90deg) } }' ) } ] @@ -219,7 +219,7 @@ describe('css-orientation-lock tests', function() { shadowId: undefined, root: document, sheet: getSheet( - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,1.5708rad) } }' + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,1.5708rad); -webkit-transform: rotate3d(0,0,1,1.5708rad) } }' ) } ] @@ -235,7 +235,7 @@ describe('css-orientation-lock tests', function() { root: document, sheet: getSheet( // this rotates by 90deg - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform:matrix(0.00,1.00,-1.00,0.00,0,0); } }' + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform:matrix(0.00,1.00,-1.00,0.00,0,0); -webkit-transform:matrix(0.00,1.00,-1.00,0.00,0,0); } }' ) } ] @@ -251,7 +251,7 @@ describe('css-orientation-lock tests', function() { root: document, sheet: getSheet( // this rotates by 90deg - '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1); } }' + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1); -webkit-transform: matrix3d(0,-1,0.00,0,1.00,0,0.00,0,0,0,1,0,0,0,0,1); } }' ) } ] From 96f7a827494ae1c7b99f99f878b8f1ecac059218 Mon Sep 17 00:00:00 2001 From: jkodu Date: Fri, 13 Dec 2019 18:06:53 +0000 Subject: [PATCH 4/5] refactor: based on code review --- lib/checks/mobile/css-orientation-lock.js | 65 ++++++++++----------- lib/checks/mobile/css-orientation-lock.json | 3 + 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/checks/mobile/css-orientation-lock.js b/lib/checks/mobile/css-orientation-lock.js index 6c8882fb5c..2df7e3bd44 100644 --- a/lib/checks/mobile/css-orientation-lock.js +++ b/lib/checks/mobile/css-orientation-lock.js @@ -1,5 +1,6 @@ /* global context */ const { cssom = undefined } = context || {}; +const { degreeThreshold = 0 } = options || {}; if (!cssom || !cssom.length) { return undefined; } @@ -10,7 +11,7 @@ const rulesGroupByDocumentFragment = groupCssomByDocument(cssom); for (const key of Object.keys(rulesGroupByDocumentFragment)) { const { root, rules } = rulesGroupByDocumentFragment[key]; - const orientationRules = getOrientaionCssMediaFeatureRules(rules); + const orientationRules = rules.filter(isMediaRuleWithOrientation); if (!orientationRules.length) { continue; } @@ -26,8 +27,7 @@ for (const key of Object.keys(rulesGroupByDocumentFragment)) { relatedElements = relatedElements.concat(elms); } - // set locked boolean - isLocked = locked; + isLocked = isLocked || locked; }); }); } @@ -65,34 +65,27 @@ function groupCssomByDocument(cssObjectModel) { } /** - * Get CSS Rules that target Orientation CSS Media Features + * Filter CSS Rules that target Orientation CSS Media Features * @param {Array} cssRules * @returns {Array} */ -function getOrientaionCssMediaFeatureRules(cssRules) { +function isMediaRuleWithOrientation({ type, cssText }) { + /** + * Filter: + * CSSRule.MEDIA_Rule + * -> https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule + */ + if (type !== 4) { + return false; + } + + /** + * Filter: + * CSSRule with conditionText of `orientation` + */ return ( - cssRules - /** - * Filter: - * CSSRule.MEDIA_Rule - * -> https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule - */ - .filter(({ type }) => type === 4) - /** - * Filter: - * CSSRule with conditionText of `orientation` - */ - .filter(({ cssText }) => { - return ( - /orientation:\s*landscape/i.test(cssText) || - /orientation:\s*portrait/i.test(cssText) - ); - }) - /** - * Filter: - * Ensure RULELIST is not empty - */ - .filter(({ cssRules }) => cssRules.length > 0) + /orientation:\s*landscape/i.test(cssText) || + /orientation:\s*portrait/i.test(cssText) ); } @@ -130,8 +123,14 @@ function getIsOrientationLocked({ selectorText, style }) { return false; } - const locked = degrees % 90 === 0 && degrees % 180 !== 0; - return !!locked; + /** + * When degree is a multiple of 180, it is not considered an orientation lock + */ + if (Math.abs(degrees) % 180 <= degreeThreshold) { + return false; + } + + return Math.abs(degrees) % 90 <= degreeThreshold; } /** @@ -168,7 +167,7 @@ function getRotationInDegrees(transformFunction, transformFnValue) { * @returns{Number|undefined} */ function getAngleInDegrees(angleWithUnit) { - const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/); + const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/) || []; if (!unit) { return; } @@ -223,7 +222,7 @@ function getAngleInDegreesFromMatrixTransform(transformFnValue) { * @return {Number} */ function convertRadToDeg(radians) { - return parseInt(radians * (180 / Math.PI)); + return Math.round(radians * (180 / Math.PI)); } /** @@ -237,7 +236,7 @@ function convertGradToDeg(grad) { if (grad < 0) { grad += 400; } - return parseInt((grad / 400) * 360); + return Math.round((grad / 400) * 360); } /** @@ -247,5 +246,5 @@ function convertGradToDeg(grad) { * @returns {Number} */ function convertTurnToDeg(turn) { - return parseInt(360 / (1 / turn)); + return Math.round(360 / (1 / turn)); } diff --git a/lib/checks/mobile/css-orientation-lock.json b/lib/checks/mobile/css-orientation-lock.json index 8bdadb0a15..93acdb2bdf 100644 --- a/lib/checks/mobile/css-orientation-lock.json +++ b/lib/checks/mobile/css-orientation-lock.json @@ -1,6 +1,9 @@ { "id": "css-orientation-lock", "evaluate": "css-orientation-lock.js", + "options": { + "degreeThreshold": 2 + }, "metadata": { "impact": "serious", "messages": { From cf70d2e74c0041f8647ebc834f810e26b4d5853f Mon Sep 17 00:00:00 2001 From: jkodu Date: Mon, 16 Dec 2019 22:12:59 +0000 Subject: [PATCH 5/5] fix: make angle calculation absolute to degree easily compare to closest denominator --- lib/checks/mobile/css-orientation-lock.js | 7 ++-- test/checks/mobile/css-orientation-lock.js | 43 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/checks/mobile/css-orientation-lock.js b/lib/checks/mobile/css-orientation-lock.js index 2df7e3bd44..5cf91c21c3 100644 --- a/lib/checks/mobile/css-orientation-lock.js +++ b/lib/checks/mobile/css-orientation-lock.js @@ -118,19 +118,20 @@ function getIsOrientationLocked({ selectorText, style }) { } const [, transformFn, transformFnValue] = matches; - const degrees = getRotationInDegrees(transformFn, transformFnValue); + let degrees = getRotationInDegrees(transformFn, transformFnValue); if (!degrees) { return false; } + degrees = Math.abs(degrees); /** * When degree is a multiple of 180, it is not considered an orientation lock */ - if (Math.abs(degrees) % 180 <= degreeThreshold) { + if (Math.abs(degrees - 180) % 180 <= degreeThreshold) { return false; } - return Math.abs(degrees) % 90 <= degreeThreshold; + return Math.abs(degrees - 90) % 90 <= degreeThreshold; } /** diff --git a/test/checks/mobile/css-orientation-lock.js b/test/checks/mobile/css-orientation-lock.js index 3ee423e72a..cba6e8b131 100644 --- a/test/checks/mobile/css-orientation-lock.js +++ b/test/checks/mobile/css-orientation-lock.js @@ -134,6 +134,27 @@ describe('css-orientation-lock tests', function() { assert.isTrue(actual); }); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of rotate, which affects rotation but does not lock orientation (rotate(-178deg))', function() { + var actual = check.evaluate.call( + checkContext, + document, + { degreeThreshold: 3 }, + undefined, + { + cssom: [ + { + shadowId: 'a', + root: document, + sheet: getSheet( + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { body { transform: rotate(-178deg); -webkit-transform: rotate(-178deg); } }' + ) + } + ] + } + ); + assert.isTrue(actual); + }); + it('returns true when CSSOM has Orientation CSS media features with transform property and tranformation function of rotateZ, which affects rotation but does not lock orientation (rotateZ(1turn))', function() { var actual = check.evaluate.call(checkContext, document, {}, undefined, { cssom: [ @@ -212,6 +233,28 @@ describe('css-orientation-lock tests', function() { assert.isFalse(actual); }); + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate3d, which affects rotation and locks orientation (rotate3d(0,0,1,93deg))', function() { + var actual = check.evaluate.call( + checkContext, + document, + { degreeThreshold: 3 }, + undefined, + { + cssom: [ + { + shadowId: undefined, + root: document, + sheet: getSheet( + // apply 0 on the z-axis (3rd parameter) does not apply given rotation on z-axis + '@media screen and (min-width: 1px) and (max-width: 3000px) and (orientation: landscape) { #mocha { transform: rotate3d(0,0,1,93deg); -webkit-transform: rotate3d(0,0,1,93deg) } }' + ) + } + ] + } + ); + assert.isFalse(actual); + }); + it('returns false when CSSOM has Orientation CSS media features with transform property and transformation function of rotate3d, which affects rotation and locks orientation (rotate3d(0,0,1,1.5708rad))', function() { var actual = check.evaluate.call(checkContext, document, {}, undefined, { cssom: [