diff --git a/.eslintrc.js b/.eslintrc.js index d5dd272f22..71a39bc1ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { root: true, extends: ['prettier'], parserOptions: { - ecmaVersion: 2021 + ecmaVersion: 2023 }, env: { node: true, diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 98d7e21f51..e4d99305f7 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -22,7 +22,7 @@ | [aria-command-name](https://dequeuniversity.com/rules/axe/4.8/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | | [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.8/aria-conditional-attr?application=RuleDescription) | Ensures ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | | [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.8/aria-deprecated-role?application=RuleDescription) | Ensures elements do not use deprecated roles | Minor | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [674b10](https://act-rules.github.io/rules/674b10) | -| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.8/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.4.1.2 | failure | | +| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.8/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden="true" is not present on the document body. | Critical | cat.aria, wcag2a, wcag131, wcag412, EN-301-549, EN-9.1.3.1, EN-9.4.1.2 | failure | | | [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.8/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements are not focusable nor contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2 | failure, needs review | [6cfa84](https://act-rules.github.io/rules/6cfa84) | | [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.8/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT5.c, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | | [aria-meter-name](https://dequeuniversity.com/rules/axe/4.8/aria-meter-name?application=RuleDescription) | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111, EN-301-549, EN-9.1.1.1 | failure, needs review | | diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index d4f773f663..5201628d38 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -1,7 +1,6 @@ import { Colorjs } from '../../core/imports'; const hexRegex = /^#[0-9a-f]{3,8}$/i; -const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; /** * @class Color @@ -12,7 +11,26 @@ const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; * @param {number} alpha */ export default class Color { + // color channel values typically in the range of 0-1 (can go below or above) + #r; + #g; + #b; + // color component values resolved to the sRGB color space (0-255) + #red; + #green; + #blue; + constructor(red, green, blue, alpha = 1) { + if (red instanceof Color) { + // preserve out of gamut values + const { r, g, b } = red; + this.r = r; + this.g = g; + this.b = b; + this.alpha = red.alpha; + return; + } + /** @type {number} */ this.red = red; @@ -26,6 +44,60 @@ export default class Color { this.alpha = alpha; } + get r() { + return this.#r; + } + + set r(value) { + this.#r = value; + this.#red = Math.round(clamp(value, 0, 1) * 255); + } + + get g() { + return this.#g; + } + + set g(value) { + this.#g = value; + this.#green = Math.round(clamp(value, 0, 1) * 255); + } + + get b() { + return this.#b; + } + + set b(value) { + this.#b = value; + this.#blue = Math.round(clamp(value, 0, 1) * 255); + } + + get red() { + return this.#red; + } + + set red(value) { + this.#r = value / 255; + this.#red = clamp(value, 0, 255); + } + + get green() { + return this.#green; + } + + set green(value) { + this.#g = value / 255; + this.#green = clamp(value, 0, 255); + } + + get blue() { + return this.#blue; + } + + set blue(value) { + this.#b = value / 255; + this.#blue = clamp(value, 0, 255); + } + /** * Provide the hex string value for the color * @method toHexString @@ -34,9 +106,9 @@ export default class Color { * @return {string} */ toHexString() { - var redString = Math.round(this.red).toString(16); - var greenString = Math.round(this.green).toString(16); - var blueString = Math.round(this.blue).toString(16); + const redString = Math.round(this.red).toString(16); + const greenString = Math.round(this.green).toString(16); + const blueString = Math.round(this.blue).toString(16); return ( '#' + (this.red > 15.5 ? redString : '0' + redString) + @@ -57,28 +129,12 @@ export default class Color { * @instance */ parseString(colorString) { - // Colorjs currently does not support rad or turn angle values - // @see https://github.com/LeaVerou/color.js/issues/311 - colorString = colorString.replace(hslRegex, (match, angle, unit) => { - const value = angle + unit; - - switch (unit) { - case 'rad': - return match.replace(value, radToDeg(angle)); - case 'turn': - return match.replace(value, turnToDeg(angle)); - } - }); - try { // srgb values are between 0 and 1 const color = new Colorjs(colorString).to('srgb'); - // when converting from one color space to srgb - // the values of rgb may be above 1 so we need to clamp them - // we also need to round the final value as rgb values don't have decimals - this.red = Math.round(clamp(color.r, 0, 1) * 255); - this.green = Math.round(clamp(color.g, 0, 1) * 255); - this.blue = Math.round(clamp(color.b, 0, 1) * 255); + this.r = color.r; + this.g = color.g; + this.b = color.b; // color.alpha is a Number object so convert it to a number this.alpha = +color.alpha; } catch (err) { @@ -137,66 +193,134 @@ export default class Color { * @return {number} The luminance value, ranges from 0 to 1 */ getRelativeLuminance() { - var rSRGB = this.red / 255; - var gSRGB = this.green / 255; - var bSRGB = this.blue / 255; + const { r: rSRGB, g: gSRGB, b: bSRGB } = this; - var r = + const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4); - var g = + const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4); - var b = + const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /** - * Add the value to a color and return a new instance of the Color. The resulting color values are not clamped to valid values of the color space (must be done separately). - * @method add + * Add a value to the color channels + * @private + * @param {number} value The value to add + * @return {Color} A new color instance + */ + #add(value) { + const C = new Color(this); + C.r += value; + C.g += value; + C.b += value; + return C; + } + + /** + * Get the luminosity of a color + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method getLuminosity * @memberof axe.commons.color.Color * @instance - * @return {Color} + * @return {number} The luminosity of the color */ - add(value) { - return new Color( - this.red + value, - this.green + value, - this.blue + value, - this.alpha - ); + getLuminosity() { + return 0.3 * this.r + 0.59 * this.g + 0.11 * this.b; } /** - * Divide a color by the value and return a new instance of the Color - * @method divide + * Set the luminosity of a color + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method setLuminosity * @memberof axe.commons.color.Color * @instance - * @return {Color} + * @param {number} L The luminosity + * @return {Color} A new color instance */ - divide(value) { - return new Color( - this.red / value, - this.green / value, - this.blue / value, - this.alpha - ); + setLuminosity(L) { + const d = L - this.getLuminosity(); + return this.#add(d).clip(); } /** - * Multiply a color by the value and return a new instance of the Color. The resulting color values are not clamped to valid values of the color space (must be done separately). - * @method multiply + * Get the saturation of a color + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method getSaturation * @memberof axe.commons.color.Color * @instance - * @return {Color} + * @return {number} The saturation of the color */ - multiply(value) { - return new Color( - this.red * value, - this.green * value, - this.blue * value, - this.alpha - ); + getSaturation() { + return Math.max(this.r, this.g, this.b) - Math.min(this.r, this.g, this.b); + } + + /** + * Set the saturation of a color + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method setSaturation + * @memberof axe.commons.color.Color + * @instance + * @param {number} s The saturation + * @return {Color} A new color instance + */ + setSaturation(s) { + const C = new Color(this); + const colorEntires = [ + { name: 'r', value: C.r }, + { name: 'g', value: C.g }, + { name: 'b', value: C.b } + ]; + + // find the min, mid, and max values of the color components + const [Cmin, Cmid, Cmax] = colorEntires.sort((a, b) => { + return a.value - b.value; + }); + + if (Cmax.value > Cmin.value) { + Cmid.value = ((Cmid.value - Cmin.value) * s) / (Cmax.value - Cmin.value); + Cmax.value = s; + } else { + Cmid.value = Cmax.value = 0; + } + + Cmin.value = 0; + + C[Cmax.name] = Cmax.value; + C[Cmin.name] = Cmin.value; + C[Cmid.name] = Cmid.value; + return C; + } + + /** + * Clip the color between RGB 0-1 accounting for the luminosity of the color. Color must be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method clip + * @memberof axe.commons.color.Color + * @instance + * @return {Color} A new color instance clipped between 0-1 + */ + clip() { + const C = new Color(this); + const L = C.getLuminosity(); + const n = Math.min(C.r, C.g, C.b); + const x = Math.max(C.r, C.g, C.b); + + if (n < 0) { + C.r = L + ((C.r - L) * L) / (L - n); + C.g = L + ((C.g - L) * L) / (L - n); + C.b = L + ((C.b - L) * L) / (L - n); + } + + if (x > 1) { + C.r = L + ((C.r - L) * (1 - L)) / (x - L); + C.g = L + ((C.g - L) * (1 - L)) / (x - L); + C.b = L + ((C.b - L) * (1 - L)) / (x - L); + } + + return C; } } @@ -204,13 +328,3 @@ export default class Color { function clamp(value, min, max) { return Math.min(Math.max(min, value), max); } - -// convert radians to degrees -function radToDeg(rad) { - return (rad * 180) / Math.PI; -} - -// convert turn to degrees -function turnToDeg(turn) { - return turn * 360; -} diff --git a/lib/commons/dom/get-visible-child-text-rects.js b/lib/commons/dom/get-visible-child-text-rects.js index 8ac6166012..02611798e8 100644 --- a/lib/commons/dom/get-visible-child-text-rects.js +++ b/lib/commons/dom/get-visible-child-text-rects.js @@ -39,8 +39,14 @@ const getVisibleChildTextRects = memoize( * @see https://github.com/dequelabs/axe-core/issues/2178 * @see https://github.com/dequelabs/axe-core/issues/2483 * @see https://github.com/dequelabs/axe-core/issues/2681 + * + * also need to resize the nodeRect to fit within the bounds of any overflow: hidden ancestors. + * + * @see https://github.com/dequelabs/axe-core/issues/4253 */ - return clientRects.length ? clientRects : [nodeRect]; + return clientRects.length + ? clientRects + : filterHiddenRects([nodeRect], overflowHiddenNodes); } ); export default getVisibleChildTextRects; diff --git a/lib/rules/color-contrast-matches.js b/lib/rules/color-contrast-matches.js index d95b1c3890..b76921bc37 100644 --- a/lib/rules/color-contrast-matches.js +++ b/lib/rules/color-contrast-matches.js @@ -4,7 +4,8 @@ import { findUpVirtual, visuallyOverlaps, getRootNode, - isInert + isInert, + getOverflowHiddenAncestors } from '../commons/dom'; import { visibleVirtual, @@ -12,6 +13,7 @@ import { sanitize, isIconLigature } from '../commons/text'; +import { rectsOverlap } from '../commons/math'; import { isDisabled } from '../commons/forms'; import { getNodeFromTree, querySelectorAll, tokenList } from '../core/utils'; @@ -147,14 +149,22 @@ function colorContrastMatches(node, virtualNode) { } } - const rects = range.getClientRects(); - for (let index = 0; index < rects.length; index++) { + const rects = Array.from(range.getClientRects()); + const clippingAncestors = getOverflowHiddenAncestors(virtualNode); + return rects.some(rect => { //check to see if the rectangle impinges - if (visuallyOverlaps(rects[index], node)) { - return true; + const overlaps = visuallyOverlaps(rect, node); + + if (!clippingAncestors.length) { + return overlaps; } - } - return false; + + const withinOverflow = clippingAncestors.some(overflowNode => { + return rectsOverlap(rect, overflowNode.boundingClientRect); + }); + + return overlaps && withinOverflow; + }); } export default colorContrastMatches; diff --git a/package-lock.json b/package-lock.json index a2d2e529ff..7b0b05e4c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "browser-driver-manager": "1.0.4", "chai": "^4.3.7", "chalk": "^4.x", - "chromedriver": "latest", + "chromedriver": "*", "clean-jsdoc-theme": "^4.2.17", "clone": "^2.1.2", "colorjs.io": "^0.5.0", @@ -6943,9 +6943,9 @@ } }, "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true, "funding": [ { @@ -8148,13 +8148,28 @@ } }, "node_modules/karma-firefox-launcher": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", - "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", "dev": true, "dependencies": { "is-wsl": "^2.2.0", - "which": "^2.0.1" + "which": "^3.0.0" + } + }, + "node_modules/karma-firefox-launcher/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/karma-ie-launcher": { @@ -12458,9 +12473,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -18387,9 +18402,9 @@ } }, "html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true }, "html-minifier-terser": { @@ -19310,13 +19325,24 @@ } }, "karma-firefox-launcher": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", - "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", "dev": true, "requires": { "is-wsl": "^2.2.0", - "which": "^2.0.1" + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "karma-ie-launcher": { @@ -22547,9 +22573,9 @@ "dev": true }, "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true }, "ua-parser-js": { diff --git a/test/commons/color/color.js b/test/commons/color/color.js index f5e0215794..1c1230e7f8 100644 --- a/test/commons/color/color.js +++ b/test/commons/color/color.js @@ -10,6 +10,40 @@ describe('color.Color', () => { assert.equal(c1.alpha, 1); }); + it('can be constructed from a Color', () => { + const c1 = new Color(4, 3, 2, 0.5); + const c2 = new Color(c1); + assert.equal(c2.red, 4); + assert.equal(c2.green, 3); + assert.equal(c2.blue, 2); + assert.equal(c2.alpha, 0.5); + }); + + it('clamps out of gamut values for red, green, blue', () => { + const c1 = new Color(-255, 0, 510, 0.5); + assert.equal(c1.red, 0); + assert.equal(c1.green, 0); + assert.equal(c1.blue, 255); + assert.equal(c1.alpha, 0.5); + }); + + it('retains out of gamut values for r, g, b', () => { + const c1 = new Color(-255, 0, 510, 0.5); + assert.equal(c1.r, -1); + assert.equal(c1.g, 0); + assert.equal(c1.b, 2); + assert.equal(c1.alpha, 0.5); + }); + + it('can be constructed from a Color preserving out of gamut values', () => { + const c1 = new Color(-255, 0, 510, 0.5); + const c2 = new Color(c1); + assert.equal(c2.r, -1); + assert.equal(c2.g, 0); + assert.equal(c2.b, 2); + assert.equal(c2.alpha, 0.5); + }); + it('has a toJSON method', () => { const c1 = new Color(255, 128, 0); assert.deepEqual(c1.toJSON(), { @@ -414,81 +448,82 @@ describe('color.Color', () => { }); }); - describe('add', () => { - it('adds the value to the color', () => { - const color = new Color(0, 0, 0, 1); - const actual = color.add(144); - assert.equal(actual.red, 144); - assert.equal(actual.green, 144); - assert.equal(actual.blue, 144); - assert.equal(actual.alpha, 1); + describe('getLuminosity', () => { + it('returns luminosity of the Color', () => { + const L = new Color(128, 128, 0, 1).getLuminosity(); + assert.equal(L, 0.44674509803921564); }); + }); - it('does not modify the original color', () => { - const color = new Color(0, 0, 0, 1); - color.add(144); - assert.equal(color.red, 0); - assert.equal(color.green, 0); - assert.equal(color.blue, 0); - assert.equal(color.alpha, 1); + describe('setLuminosity', () => { + it('sets the luminosity of the Color', () => { + const color = new Color(0, 0, 0, 1).setLuminosity(0.5); + assert.deepEqual(color.toJSON(), { + red: 128, + green: 128, + blue: 128, + alpha: 1 + }); }); - it('does not clamp the value', () => { - const color = new Color(0, 0, 0, 1); - const actual = color.add(3000); - assert.equal(actual.red, 3000); - assert.equal(actual.green, 3000); - assert.equal(actual.blue, 3000); - assert.equal(actual.alpha, 1); + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.setLuminosity(0.5); + assert.notEqual(black, nBlack); }); }); - describe('divide', () => { - it('divides the color by the value', () => { - const color = new Color(144, 144, 144, 1); - const actual = color.divide(2); - assert.equal(actual.red, 72); - assert.equal(actual.green, 72); - assert.equal(actual.blue, 72); - assert.equal(actual.alpha, 1); - }); - - it('does not modify the original color', () => { - const color = new Color(144, 144, 144, 1); - color.divide(2); - assert.equal(color.red, 144); - assert.equal(color.green, 144); - assert.equal(color.blue, 144); - assert.equal(color.alpha, 1); + describe('getSaturation', () => { + it('returns the saturation of the Color', () => { + const s = new Color(255, 128, 200, 1).getSaturation(); + assert.equal(s, 0.4980392156862745); }); }); - describe('multiply', () => { - it('multiplies the color by the value', () => { - const color = new Color(72, 72, 72, 1); - const actual = color.multiply(2); - assert.equal(actual.red, 144); - assert.equal(actual.green, 144); - assert.equal(actual.blue, 144); - assert.equal(actual.alpha, 1); - }); - - it('does not modify the original color', () => { - const color = new Color(72, 72, 72, 1); - color.multiply(2); - assert.equal(color.red, 72); - assert.equal(color.green, 72); - assert.equal(color.blue, 72); - assert.equal(color.alpha, 1); + describe('setSaturation', () => { + it('sets the saturation of the Color', () => { + const color = new Color(128, 100, 0, 1).setSaturation(0.8); + assert.deepEqual(color.toJSON(), { + red: 204, + green: 159, + blue: 0, + alpha: 1 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.setSaturation(0.5); + assert.notEqual(black, nBlack); + }); + }); + + describe('clip', () => { + it('clips to the lower bound', () => { + const color = new Color(255, 0, -1, 1).clip(); + assert.equal(color.r, 0.9909493297254295); + assert.equal(color.g, 0.003870895819239939); + assert.equal(color.b, 0); + }); + + it('clips to the upper bound', () => { + const color = new Color(255, 0, 256, 1).clip(); + assert.equal(color.r, 0.9961043436801178); + assert.equal(color.g, 0.002711982110142841); + assert.equal(color.b, 1); }); - it('does not clamp the value', () => { - const color = new Color(30, 30, 30, 1); - const actual = color.multiply(100); - assert.equal(actual.red, 3000); - assert.equal(actual.green, 3000); - assert.equal(actual.blue, 3000); - assert.equal(actual.alpha, 1); + it('clips both the lower and upper bounds', () => { + const color = new Color(-1, 0, 256, 1).clip(); + assert.equal(color.r, 0.00047889410870861904); + assert.equal(color.g, 0.004247986549875488); + assert.equal(color.b, 0.9691356514885925); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.clip(); + assert.notEqual(black, nBlack); }); }); }); diff --git a/test/commons/color/get-stroke-colors-from-shadows.js b/test/commons/color/get-stroke-colors-from-shadows.js index ec22a85f2d..ef7e51abdb 100644 --- a/test/commons/color/get-stroke-colors-from-shadows.js +++ b/test/commons/color/get-stroke-colors-from-shadows.js @@ -14,14 +14,13 @@ describe('axe.commons.color.getStrokeColorsFromShadow', () => { -2px 0 #F00 `); const shadowColors = getStrokeColorsFromShadows(shadows); - assert.deepEqual(shadowColors, [ - { - red: 255, - green: 0, - blue: 0, - alpha: 1 - } - ]); + assert.lengthOf(shadowColors, 1); + assert.deepEqual(shadowColors[0].toJSON(), { + red: 255, + green: 0, + blue: 0, + alpha: 1 + }); }); it('returns empty when only one side is covered by the shadow', () => { diff --git a/test/commons/color/get-text-shadow-colors.js b/test/commons/color/get-text-shadow-colors.js index eb46db25ff..a9a7900162 100644 --- a/test/commons/color/get-text-shadow-colors.js +++ b/test/commons/color/get-text-shadow-colors.js @@ -202,14 +202,13 @@ describe('axe.commons.color.getTextShadowColors', function () { ">Hello world `; const shadowColors = getTextShadowColors(fixture.firstElementChild, opt); - assert.deepEqual(shadowColors, [ - { - red: 255, - green: 0, - blue: 0, - alpha: 1 - } - ]); + assert.lengthOf(shadowColors, 1); + assert.deepEqual(shadowColors[0].toJSON(), { + red: 255, + green: 0, + blue: 0, + alpha: 1 + }); }); it('only combines shadows thinner than minRatio', () => { diff --git a/test/commons/dom/get-visible-child-text-rects.js b/test/commons/dom/get-visible-child-text-rects.js index 126e79d037..844e8ea3fb 100644 --- a/test/commons/dom/get-visible-child-text-rects.js +++ b/test/commons/dom/get-visible-child-text-rects.js @@ -120,4 +120,20 @@ describe('dom.getVisibleChildTextRects', () => { assert.lengthOf(actual, 2); }); + + it('changes nodeRect size if all text rects got outside ancestor overflow', () => { + fixtureSetup(` +
+
+
Hello World
+
+
+ `); + const node = fixture.querySelector('#target'); + const actual = getVisibleChildTextRects(node); + const rect = getClientRects(node)[0]; + const expected = new DOMRect(rect.left, rect.top, 25, rect.height); + + assertRectsEqual(actual, [expected]); + }); }); diff --git a/test/integration/rules/color-contrast/color-contrast.html b/test/integration/rules/color-contrast/color-contrast.html index 5a6cc1e690..f866b05ca1 100644 --- a/test/integration/rules/color-contrast/color-contrast.html +++ b/test/integration/rules/color-contrast/color-contrast.html @@ -459,3 +459,9 @@ > Hello world + +
+
+
Hello World
+
+
diff --git a/test/rule-matches/color-contrast-matches.js b/test/rule-matches/color-contrast-matches.js index 382457119c..fc41e301ee 100644 --- a/test/rule-matches/color-contrast-matches.js +++ b/test/rule-matches/color-contrast-matches.js @@ -418,6 +418,19 @@ describe('color-contrast-matches', function () { assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); }); + it('should not match text outside overflow', () => { + fixture.innerHTML = ` +
+
+
Hello World
+
+
+ `; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(target))); + }); + if (shadowSupport) { it('should match a descendant of an element across a shadow boundary', function () { fixture.innerHTML =