Skip to content

Commit

Permalink
Palettes, text opacity, and readme
Browse files Browse the repository at this point in the history
+ Fabio Crameri, Brewer/Harrower, and Jamie Nunez palettes
+ Added text opacity support for assessments
+ Experimental JzAzBz color space metric
+ Swift 5.4
+ Clearer documentation
+ Readme
  • Loading branch information
importRyan committed Mar 4, 2021
1 parent 492a8e9 commit fecbfb0
Show file tree
Hide file tree
Showing 58 changed files with 3,238 additions and 260 deletions.
161 changes: 114 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,146 @@
# InclusiveColor **Beta**
![Logo](https://i.imgur.com/rYIWrEv.png "Logo")
<br>

This is a humble toolset:
1. `AssertInclusive` Unit tests colors and text against accessibility standards across color vision simulations
2. `ICPalettes` Contains free accessibility-friendly color palettes published by researchers and engineers
3. `.simulate()` For internal UI testing, adapts UIColor, NSColor, and Color objects to simulate color vision impairments
4. `.caption()` Captions color objects for accessibility tags, if the need arises
* [Quick start](#quick-start)
* [Companion macOS app](#companion-macos-app)
* [Why unit test for color accessibility?](#why-unit-test-color-accessibility)
* [Swift Package Manager](#swift-package-manager)
* [Troubleshooting](#troubleshooting)
* [Feedback](#feedback)
* [About WCAG](#about-wcag)
<br>

A free companion macOS app:
1. Visualizes test results for color sets and fonts across multiple metrics and vision simulations, helping designers respond to unit test screen failures
2. Assembles palettes quickly by importing Swift code, xcasset drops, manual inputs, or by cursor sampling + a key trigger
3. Exports colors as Swift code
4. Houses more documentation for this package

-------------------------------------------------------------------------------------------------------------
Quick start
=====================================

Safeguard your app’s meaningful colors and text–background combos by quick unit tests against the WCAG 2.1 guidelines. Test a few buttons in a view model or all color combinations in a theme.

## Why unit test for color accessibility?
1. About 8% of men and 0.5% of women are somewhat or completely unable to see colors that could be essential to your UI.
2. Indistinguishable colors may surprise you. (It's not just red vs. green.)
3. An ~50-200 ms unit test can screen thousands of color combinations... even on Intel. That beats screening or finding regressions by sporadic manual inspection.
```swift
import InclusiveColor

class YourTests: XCTestCase {

func test_IconStates_VisibleToColorblindUsers() {
let colors = [NSColor.yellow, .green] // Or UIColor/Color arrays
AssertInclusive(colors: colors)
}

func test_ButtonLabels_MeetTextContrastMinimum() {
let text = [NSColor.textColor, .secondaryLabelColor]
let backgrounds = [NSColor.systemIndigo]
let font = ICFontStyle(.title3)

AssertInclusive(text: text,
backgrounds: backgrounds,
fonts: [font])
}
}
```
![TestFailures](https://i.imgur.com/pmOjkWN.png "XCTest failure messages")

> No surprise, static yellow and green are inaccessible meaningful colors per [the WCAG](#about-wcag), which requires a luminance ratio of ≥3. As for the indigo button: while the white textColor is fine, the 55% opacity secondaryLabelColor just needs a little bump.
## Quick start
* **Be more stringent** — Apply WCAG’s more accessible “enhanced” text contrast standard by setting the `metric` parameter in `AssertInclusive`.

* Import this package using Swift Package Manager.
* Add `InclusiveColor` to your unit test target(s). (Go to `Build Phase`, `Link Binary With Libraries`).
```
https://github.com/importRyan/InclusiveColor
```
* **Free accessible color palettes** — Try >75 palettes by scientists and designers namespaced under `ICPalettes` along with license text. See them in the [companion macOS app](#companion-macos-app), alongside simulations and tests for colors imported from your own Swift code.
<br><br><br><br>

**Test meaningful colors (e.g., charts, toggles, or buttons)**
```
import XCTest
import InclusiveColor

let sut: [NSColor] / [UIColor] / [Color] = [.blue, .green, .white]
AssertInclusive(colors: test, pairings: .allPairs)
```
That sample test will fail with details about the inaccessible color combos. The default metric is from WCAG 2.1, but you can specify an override.

**Test text–background–font combinations**
-------------------------------------------------------------------------------------------------------------
Companion macOS app
=====================================

🧐 Visualize your palettes and test scores across color visions

🎨 Browse and fork free accessible palettes (for aesthetics and complex data)

👩🏻‍💻 Swift code import/export

🖥 Batch capture new palettes by cursor + keyboard triggers

🌈 Adjust colors in simulated color vision (pick/preview)

✉️ importRyan@gmail.com to beta test

![Screenshot](https://i.imgur.com/jERJ0u4.png "Companion macOS App screenshot")
<br><br><br>


-------------------------------------------------------------------------------------------------------------
Why unit test color accessibility?
=====================================

1. **8% of men and 0.5% of women** are somewhat or completely unable to see colors that could be essential to your UI.
2. Indistinguishable colors may surprise you. (It's not just red vs. green.)
3. An ~50-200 ms unit test can screen thousands of color combinations. That beats screening or finding regressions by sporadic manual inspection.

![Simulations](https://i.imgur.com/eYX7q76.png "Simulated HSV Wheels")
<br><br><br><br>



-------------------------------------------------------------------------------------------------------------
Swift Package Manager
=====================================
Support for Cocoapod and Carthage is coming soon. For now, use SPM:

```
AssertInclusive(text: textColorArray,
backgrounds: bgColorArray,
fonts: .wcag2_body(),
inclusivity: .max99percent)
https://github.com/importRyan/InclusiveColor
```
The `fonts` parameter has defaults (and explanations) for WCAG body and strong text, but also accepts custom styles and applies the appropriate standard.

The `inclusivity` parameter on these assertions by default covers typical, protan, deutan, tritan, and monochromatic vision. Other options restrict testing to vision types covering less of your user base.

**Wrap your own assertions**
Similar to the new assertion tools in Swift 5.4, you can silence failure handlers in favor of your own assertions, such as expecting a failure while waiting on a design fix.
:warning: Only link this library to unit test targets because it extends `XCTest`.

Assign `AssertInclusive` to a variable. Pass `true` for the optional parameter `suppressFailure`. The now-silenced assertion will output a tuple that includes an overall didPass verdict, details on comparisons and simulations, and its usual failure description.
All logic is housed in a second library, `InclusiveColorTools` that is safe to link to executables, such as for live simulation of colors during development using `.simulate(for:)`.
<br><br><br><br>

Domain experts can further customize framework behaviors. For example, you can replace the vision simulator (based on Machado et al) with preferred transforms as a parameter for individual assertions or as a global default via the `InclusiveColorTools.setDefaultSimulator` method.


### Build error?
The `InclusiveColorTools` library does not extend `XCTest`. Production targets can link to it.
-------------------------------------------------------------------------------------------------------------
Troubleshooting
=====================================
#### 💥 Build error?
Presently, Xcode and Swift Package Manger do not gracefully ignore an `XCTest` import in linked libraries. The workaround is simple: only link such a library to your unit test targets.

But `InclusiveColor` wraps `Tools` with custom `XCTest` assertions. Presently, Xcode and Swift Package Manger do not gracefully ignore an `XCTest` import. The workaround is simple: only link this library to unit test targets.
This framework has two libraries. The namesake library extends `XCTest` by wrapping the logic housed in the other library, `InclusiveColorTools`, with pretty-printing assertion handlers.

**To solve build errors**
1. Tap on your `Project` and then the desired unit test under `Targets`
2. In `Build Phases`, open `Link Binary With Libraries`
3. Use the `+` to add the `InclusiveColor` library
4. If build errors continue, you may need to remove `InclusiveColor` from
* `Test Targets\Build Phases\Dependencies`
* `Executable Targets\Build Phases\*`
<br><br><br><br>



## Feedback

Find a bug? Wish I wrote a better API? Suggestions on metrics or other features?
-------------------------------------------------------------------------------------------------------------
Feedback
=====================================
Bug? Wish I wrote a better API? Suggested feature or metric?

Please email me importRyan@gmail.com or open an issue/PR on GitHub. Feedback from experienced developers to enhance the utility of this package would be welcomed warmly, as I am looking for my first junior dev role.
<br><br><br><br>



-------------------------------------------------------------------------------------------------------------
About WCAG
=====================================
The Web Content Accessibility Guidelines are a common standard for accessibility testing for web content. It has far more resources than color contrast alone. Apple's own Accessibility Inspector tool with Xcode reports WCAG color contrast for elements in your app.

* [WCAG About](https://www.w3.org/WAI/standards-guidelines/wcag/)
* [WCAG "Quick" Reference Guide](https://www.w3.org/WAI/WCAG21/quickref/)
* [1.4.3 Minimum Contrast Criterion](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html)
* [1.4.6 Enhanced Contrast Criterion](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html)


### Why show WCAG scores for simulated color vision?
Color vision simulations will occasionally suggest that some pairs would benefit from a little extra contrast to keep everyone above a common minimum.

The WCAG 2.1 algorithm uses gray luminance as a performant shortcut to a lowest common denominator. But human color perception is not a monolithic light calculator, nor does everyone process wavelengths like the employed luma formula does. Light perception is a product of multiple mechanisms from photoreceptors to pre-attentive cortical processing.

It’s likely the currently active WCAG3 working group or other color scientists will develop more nuanced measures with empirical evidence. This is just a helpful extra measure along the way.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ public extension XCTestCase {
/// - exp: Expected color
/// - sut: Test result color
/// - rgb888Tolerance: Accuracy affordance as an 8-bit channel value (e.g., 1 or 2.5). Allowance is per-channel. Default is 1.
/// - alpha8Tolerance: Affordance specifically for the alpha channel. Default is zero. The framework should not modify or lose track of an input alpha channel.
/// - alpha8Tolerance: Affordance specifically for the alpha channel. Default is zero.
/// - index: Optional: Index of a color in an input array for inclusion in failure messages for more convenient tracing of errors.
/// - label: Optional: Convenience label for inclusion in failure messages (e.g., the vision type simulated).
/// - file: Passes call site for displaying the failure on the appropriate test case
/// - line: Passes call site for displaying the failure on the appropriate line
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself. Swift 5.4 offers built-in methods for finer control of failures.
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself.
///
/// - Returns: Tuple: isComparable result, summary of failed comparison
///
Expand Down
30 changes: 22 additions & 8 deletions Sources/InclusiveColor/Assertions/AssertColorsComparable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ public extension XCTestCase {
/// - exp: Expected color
/// - sut: Test result color
/// - rgb888Tolerance: Accuracy affordance as an 8-bit channel value (e.g., 1 or 2.5). Allowance is per-channel. Default is 1.
/// - alpha8Tolerance: Affordance specifically for the alpha channel. Default is zero. The framework should not modify or lose track of an input alpha channel.
/// - alpha8Tolerance: Affordance specifically for the alpha channel. Default is zero.
/// - index: Optional: Index of a color in an input array for inclusion in failure messages for more convenient tracing of errors.
/// - label: Optional: Convenience label for inclusion in failure messages (e.g., the vision type simulated).
/// - file: Passes call site for displaying the failure on the appropriate test case
/// - line: Passes call site for displaying the failure on the appropriate line
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself. Swift 5.4 offers built-in methods for finer control of failures.
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself.
///
/// - Returns: Tuple: isComparable result, summary of failed comparison
///
Expand All @@ -35,9 +35,9 @@ public extension XCTestCase {
let toleranceRGB888: ICColorChannel = abs(Float(rgb888Tolerance) / 255).clamped01() + .ulpOfOne
let toleranceA8: ICColorChannel = abs(Float(alpha8Tolerance) / 255).clamped01() + .ulpOfOne

let red = abs(exp.rgb[0].distance(to: sut.rgb[0])) < toleranceRGB888
let green = abs(exp.rgb[0].distance(to: sut.rgb[0])) < toleranceRGB888
let blue = abs(exp.rgb[0].distance(to: sut.rgb[0])) < toleranceRGB888
let red = abs(exp.rgb.red.distance(to: sut.rgb.red)) < toleranceRGB888
let green = abs(exp.rgb.green.distance(to: sut.rgb.green)) < toleranceRGB888
let blue = abs(exp.rgb.blue.distance(to: sut.rgb.blue)) < toleranceRGB888
let isAlphaEqual = abs(exp.a.distance(to: sut.a)) < toleranceA8

if red && green && blue && isAlphaEqual { return (true, "") }
Expand Down Expand Up @@ -66,7 +66,21 @@ public extension XCTestCase {
return (false, message)
}


/// Compares color equality by channel values, affording a tolerance default of 1 8-bit value per channel. Reports failures by index (if available) and a terse 8-bit RGB description of the expected and test result colors.
///
/// - Parameters:
/// - exp: Expected color vector
/// - sut: Test result color vector
/// - rgb888Tolerance: Accuracy affordance as an 8-bit channel value (e.g., 1 or 2.5). Allowance is per-channel. Default is 1.
/// - alpha8Tolerance: Affordance specifically for the alpha channel. Default is zero.
/// - index: Optional: Index of a color in an input array for inclusion in failure messages for more convenient tracing of errors.
/// - label: Optional: Convenience label for inclusion in failure messages (e.g., the vision type simulated).
/// - file: Passes call site for displaying the failure on the appropriate test case
/// - line: Passes call site for displaying the failure on the appropriate line
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself.
///
/// - Returns: Tuple: isComparable result, summary of failed comparison
///
@discardableResult
func AssertColorsComparable(exp: sRGBColor,
sut: sRGBColor,
Expand All @@ -83,8 +97,8 @@ public extension XCTestCase {
let toleranceRGB888: ICColorChannel = (Float(rgb888Tolerance) / 255).clamped01() + .ulpOfOne

let red = abs(exp[0].distance(to: sut[0])) < toleranceRGB888
let green = abs(exp[0].distance(to: sut[0])) < toleranceRGB888
let blue = abs(exp[0].distance(to: sut[0])) < toleranceRGB888
let green = abs(exp[1].distance(to: sut[1])) < toleranceRGB888
let blue = abs(exp[2].distance(to: sut[2])) < toleranceRGB888

if red && green && blue { return (true, "") }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public extension XCTestCase {
/// - message: Optional message to include in the failed assertion flag
/// - file: Test file
/// - line: Assertion line
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself. Swift 5.4 offers built-in methods for finer control of failures.
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself.
///
/// - Returns: Discardable tuple
/// - Bool: Did pass inclusivity requirement for all chosen vision types
Expand Down
10 changes: 7 additions & 3 deletions Sources/InclusiveColor/Assertions/AssertInclusive+Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import XCTest

public extension XCTestCase {

/// Assess backgrounds, text colors, and font styles against an accessibility standard across the chosen vision types. By default, all vision types are assessed using the WCAG 2.1 Minimum Contrast 1.4.3 AA criterion, which demands a minimum relative luminance ratio of 4.5:1 for body text and 3:1 for strong text (i.e., at least 18 pt or 14 pt and bold). There are stricter standards than 1.4.3 AA.
/// Assess backgrounds, text colors, and font styles against an accessibility standard across the chosen vision types.
///
/// By default, all vision types are assessed using the WCAG 2.1 Minimum Contrast 1.4.3 AA criterion. Passing requires a minimum relative luminance ratio of 4.5:1 for body text and 3:1 for strong text. The WCAG defines strong text as at least 18 pt or 14 pt and bold.
///
/// To use the WCAG's "Enhanced" standard for higher contrast requirements, set the `metric` parameter.
///
/// - Parameters:
/// - text: Any Swift color object
Expand All @@ -15,15 +19,15 @@ public extension XCTestCase {
/// - message: Optional message to include in the failed assertion flag
/// - file: Test file
/// - line: Assertion line
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself. Swift 5.4 offers built-in methods for finer control of failures.
/// - suppressFailure: Default is false. Pass true to collect the error message and pass/fail state, but not trigger the failure handler so you may express an XCTest failure yourself.
///
/// - Returns: An object that reports:
/// * if all background, text, and font combinations did pass the selected standard
/// * colors simulated for requested vision types
/// * detailed pairwise contrast comparisons
/// * a basic statistical summary
///
/// - Warning: Currently, extended sRGB color space inputs are clamped into 0...1 sRGB values.
/// - Warning: Extended sRGB inputs are clamped into standard sRGB for compatibility with several simulation algorithms.
///
/// - Returns: Discardable tuple
/// - Bool: Did pass inclusivity requirement for all chosen vision types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Foundation
/// * detailed pairwise contrast comparisons
/// * a basic statistical summary
///
/// - Warning: Currently, extended sRGB color space inputs are clamped into 0...1 sRGB values.
/// - Warning: Extended sRGB inputs are clamped into standard sRGB for compatibility with several simulation algorithms.
///
public func assess<C: ICAnyColor>(colors: [C],
pairings pairingStrategy: ICColorPairingStrategy = .allPairs,
Expand Down
Loading

0 comments on commit fecbfb0

Please sign in to comment.