From 3d6b0a835ee1a3ce7175aabdb1ac839e62517798 Mon Sep 17 00:00:00 2001 From: azurepolarbear Date: Sat, 1 Jun 2024 21:50:21 -0500 Subject: [PATCH] #14 #88 Complete color-contrast-assessor.ts class and unit tests. --- docs/release-notes/v0.6.0-notes.md | 64 +++ jest.config.ts | 1 + .../color-contrast/color-contrast-assessor.ts | 26 +- .../color-contrast-assessor.test.ts | 423 ++++++++++++++++++ 4 files changed, 502 insertions(+), 12 deletions(-) create mode 100644 src/test/color/color-contrast/color-contrast-assessor.test.ts diff --git a/docs/release-notes/v0.6.0-notes.md b/docs/release-notes/v0.6.0-notes.md index 3ad929e..8d81349 100644 --- a/docs/release-notes/v0.6.0-notes.md +++ b/docs/release-notes/v0.6.0-notes.md @@ -240,6 +240,70 @@ declare interface Palette { # New Classes +## `ColorContrastAssessor` + +```typescript +/** + * Evaluates if two colors meet the AA or AAA contrast standard + * of the Web Content Accessibility Guidelines (WCAG).
+ * To learn more about WCAG, visit + * https://www.w3.org/WAI/standards-guidelines/wcag/. + * + * @category Color + * @category Color Contrast + */ +declare class ColorContrastAssessor { + public static meetsContrastStandard(colorA: Color, + colorB: Color): boolean; + public static meetsContrastStandard(colorA: PaletteColor, + colorB: PaletteColor): boolean; + public static meetsContrastStandard(colorA: string, + colorB: string): boolean; + public static meetsContrastStandard(colorA: Color, + colorB: Color, + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; + public static meetsContrastStandard(colorA: PaletteColor, + colorB: PaletteColor, + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; + public static meetsContrastStandard(colorA: string, + colorB: string, + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; + /** + * Evaluates if two colors have an appropriate contrast ratio + * for the given {@link ContrastStandard} and {@link ContrastFontSize}. + * + * @param colorA + * @param colorB + * @param standard - If no standard is provided, {@link ContrastStandard.AA} will be used. + * @param fontSize - If no font size is provided, {@link ContrastFontSize.NORMAL} will be used. + */ + public static meetsContrastStandard(colorA: Color | PaletteColor | string, + colorB: Color | PaletteColor | string, + standard?: ContrastStandard, + fontSize?: ContrastFontSize): boolean; + + /** + * Do the given colors conform to guidelines for the given standard and font size? + * + * @param hexA + * @param hexB + * @param standard + * @param fontSize + * + * @returns `true` if the two colors have an acceptable contrast ratio + * for the given {@link ContrastStandard} and {@link ContrastFontSize}, + * `false` if they do not have an acceptable ratio. + */ + private static haveAppropriateContrastRatio(hexA: string, + hexB: string, + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; +} +``` + ## `ColorNameManager` ```typescript diff --git a/jest.config.ts b/jest.config.ts index d18a3d6..4577bfd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -25,6 +25,7 @@ const config: JestConfigWithTsJest = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json', 'node'], moduleNameMapper: { '^color$': '/src/main/color', + '^color-contrast$': '/src/main/color/color-contrast', '^context$': '/src/main/p5', '^discriminator$': '/src/main/discriminator', '^map$': '/src/main/map', diff --git a/src/main/color/color-contrast/color-contrast-assessor.ts b/src/main/color/color-contrast/color-contrast-assessor.ts index 3e077e8..f75ccc8 100644 --- a/src/main/color/color-contrast/color-contrast-assessor.ts +++ b/src/main/color/color-contrast/color-contrast-assessor.ts @@ -57,8 +57,6 @@ export enum ContrastFontSize { LARGE = 'large' } -// TODO - unit tests -// TODO - add to release notes /** * Evaluates if two colors meet the AA or AAA contrast standard * of the Web Content Accessibility Guidelines (WCAG).
@@ -69,19 +67,24 @@ export enum ContrastFontSize { * @category Color Contrast */ export class ColorContrastAssessor { - - // TODO - unit tests + public static meetsContrastStandard(colorA: Color, + colorB: Color): boolean; + public static meetsContrastStandard(colorA: PaletteColor, + colorB: PaletteColor): boolean; + public static meetsContrastStandard(colorA: string, + colorB: string): boolean; public static meetsContrastStandard(colorA: Color, colorB: Color, - standard?: ContrastStandard, - fontSize?: ContrastFontSize): boolean; + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; public static meetsContrastStandard(colorA: PaletteColor, colorB: PaletteColor, - standard?: ContrastStandard, - fontSize?: ContrastFontSize): boolean; - public static meetsContrastStandard(colorA: string, colorB: string, - standard?: ContrastStandard, - fontSize?: ContrastFontSize): boolean; + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; + public static meetsContrastStandard(colorA: string, + colorB: string, + standard: ContrastStandard, + fontSize: ContrastFontSize): boolean; /** * Evaluates if two colors have an appropriate contrast ratio * for the given {@link ContrastStandard} and {@link ContrastFontSize}. @@ -117,7 +120,6 @@ export class ColorContrastAssessor { ); } - // TODO - unit tests /** * Do the given colors conform to guidelines for the given standard and font size? * diff --git a/src/test/color/color-contrast/color-contrast-assessor.test.ts b/src/test/color/color-contrast/color-contrast-assessor.test.ts new file mode 100644 index 0000000..553d8f5 --- /dev/null +++ b/src/test/color/color-contrast/color-contrast-assessor.test.ts @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2024 brittni and the polar bear LLC. + * + * This file is a part of brittni and the polar bear's Generative Art Library, + * which is released under the GNU Affero General Public License, Version 3.0. + * You may not use this file except in compliance with the license. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. See LICENSE or go to + * https://www.gnu.org/licenses/agpl-3.0.en.html for full license details. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + */ + +import {Color} from 'color'; +import {ColorContrastAssessor, ContrastFontSize, ContrastStandard} from 'color-contrast'; +import {SketchContext} from 'context'; +import {_0437F2, _0FFF4F, _121212, _FF6BB5} from 'palette-colors'; +import P5Lib from 'p5'; + +const p5: P5Lib = SketchContext.p5; + +describe('color contrast assessor tests', (): void => { + test.each([ + { + colorA: new Color(_0437F2), + colorB: new Color(_0437F2), + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: new Color(_0437F2), + colorB: new Color(_0FFF4F), + AA_normal: true, + AA_large: true, + AAA_normal: false, + AAA_large: true + }, + { + colorA: new Color(_0437F2), + colorB: new Color(_121212), + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: new Color(_FF6BB5), + colorB: new Color(_121212), + AA_normal: true, + AA_large: true, + AAA_normal: true, + AAA_large: true + }, + { + colorA: new Color(p5.color('#61CE51')), + colorB: new Color(p5.color('#A02DAE')), + AA_normal: false, + AA_large: true, + AAA_normal: false, + AAA_large: false + } + ])('$# contrast: Colors - $colorA._name and $colorB._name', + ({colorA, + colorB, + AA_normal, + AA_large, + AAA_normal, + AAA_large}): void => { + // AB Contrasts + const contrast_ab_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB + ); + const contrast_ab_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ab_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + // BA Contrasts + const contrast_ba_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA + ); + const contrast_ba_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ba_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + expect(contrast_ab_default).toBe(AA_normal); + expect(contrast_ba_default).toBe(AA_normal); + + expect(contrast_ab_AA_normal).toBe(AA_normal); + expect(contrast_ba_AA_normal).toBe(AA_normal); + + expect(contrast_ab_AA_large).toBe(AA_large); + expect(contrast_ba_AA_large).toBe(AA_large); + + expect(contrast_ab_AAA_normal).toBe(AAA_normal); + expect(contrast_ba_AAA_normal).toBe(AAA_normal); + + expect(contrast_ab_AAA_large).toBe(AAA_large); + expect(contrast_ba_AAA_large).toBe(AAA_large); + } + ); + + test.each([ + { + colorA: _0437F2, + colorB: _0437F2, + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: _0437F2, + colorB: _0FFF4F, + AA_normal: true, + AA_large: true, + AAA_normal: false, + AAA_large: true + }, + { + colorA: _0437F2, + colorB: _121212, + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: _FF6BB5, + colorB: _121212, + AA_normal: true, + AA_large: true, + AAA_normal: true, + AAA_large: true + } + ])('$# contrast: PaletteColors - $colorA.HEX and $colorB.HEX', + ({colorA, + colorB, + AA_normal, + AA_large, + AAA_normal, + AAA_large}): void => { + // AB Contrasts + const contrast_ab_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB + ); + const contrast_ab_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ab_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + // BA Contrasts + const contrast_ba_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA + ); + const contrast_ba_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ba_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + expect(contrast_ab_default).toBe(AA_normal); + expect(contrast_ba_default).toBe(AA_normal); + + expect(contrast_ab_AA_normal).toBe(AA_normal); + expect(contrast_ba_AA_normal).toBe(AA_normal); + + expect(contrast_ab_AA_large).toBe(AA_large); + expect(contrast_ba_AA_large).toBe(AA_large); + + expect(contrast_ab_AAA_normal).toBe(AAA_normal); + expect(contrast_ba_AAA_normal).toBe(AAA_normal); + + expect(contrast_ab_AAA_large).toBe(AAA_large); + expect(contrast_ba_AAA_large).toBe(AAA_large); + } + ); + + test.each([ + { + colorA: '#0437F2', + colorB: '#0437F2', + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: '#0437F2', + colorB: '#0FFF4F', + AA_normal: true, + AA_large: true, + AAA_normal: false, + AAA_large: true + }, + { + colorA: '#0437F2', + colorB: '#121212', + AA_normal: false, + AA_large: false, + AAA_normal: false, + AAA_large: false + }, + { + colorA: '#FF6BB5', + colorB: '#121212', + AA_normal: true, + AA_large: true, + AAA_normal: true, + AAA_large: true + }, + { + colorA: '#61CE51', + colorB: '#A02DAE', + AA_normal: false, + AA_large: true, + AAA_normal: false, + AAA_large: false + } + ])('$# contrast: hex strings - $colorA and $colorB', + ({colorA, + colorB, + AA_normal, + AA_large, + AAA_normal, + AAA_large}): void => { + // AB Contrasts + const contrast_ab_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB + ); + const contrast_ab_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ab_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ab_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorA, + colorB, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + // BA Contrasts + const contrast_ba_default: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA + ); + const contrast_ba_AA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AA, + ContrastFontSize.LARGE + ); + const contrast_ba_AAA_normal: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.NORMAL + ); + const contrast_ba_AAA_large: boolean = + ColorContrastAssessor.meetsContrastStandard( + colorB, + colorA, + ContrastStandard.AAA, + ContrastFontSize.LARGE + ); + + expect(contrast_ab_default).toBe(AA_normal); + expect(contrast_ba_default).toBe(AA_normal); + + expect(contrast_ab_AA_normal).toBe(AA_normal); + expect(contrast_ba_AA_normal).toBe(AA_normal); + + expect(contrast_ab_AA_large).toBe(AA_large); + expect(contrast_ba_AA_large).toBe(AA_large); + + expect(contrast_ab_AAA_normal).toBe(AAA_normal); + expect(contrast_ba_AAA_normal).toBe(AAA_normal); + + expect(contrast_ab_AAA_large).toBe(AAA_large); + expect(contrast_ba_AAA_large).toBe(AAA_large); + } + ); +});