Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
Beta to build companion app
  • Loading branch information
importRyan committed Feb 7, 2021
1 parent 6b1c129 commit 492a8e9
Show file tree
Hide file tree
Showing 169 changed files with 12,078 additions and 0 deletions.
50 changes: 50 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# License

This project is free to use in commercial/non-commercial works (with simple attribution) under the standard BSD three-clause license below. Please consider a donation to an accessibility-focused charity's general or educational fund.

```
Copyright 2021 by Ryan Ferrell.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```


This library was inspired by Laura Ragone's try! Swift Tokyo 2017 talk while making a board game for mixed color-blind couples and families. I forked her Inclusive-Color repo to expand her visualizations to unit testing. That snowballed into exploring the draft WCAG 3 metrics and learning some linear algebra to implement new color vision simulations.

Her original copyright and repo are below.

```
https://github.com/lauraragone/Inclusive-Color
```

```
MIT License
Copyright (c) 2018 lauraragone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
41 changes: 41 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// swift-tools-version:5.3

import PackageDescription

let package = Package(
name: "InclusiveColor",
platforms: [
.iOS(.v10),
.macOS(.v10_13),
.tvOS(.v10),
.watchOS(.v6)
],
products: [

// These two products have different uses.
//
// (1) Core tools are in "InclusiveColorTools". You can link this to production executables.
//
// (2) For unit testing targets only, "InclusiveColor" wraps the InclusiveColorTools' methods with custom assertions and error handling.
//
// Linking the latter XCTest-extending library to production targets will cause build errors. How Swift Package Manager chooses to handle XCTest extensions requires that little linking workaround and a symlink between the libraries within this package.

.library(
name: "InclusiveColor",
targets: ["InclusiveColor"]),
.library(
name: "InclusiveColorTools",
targets: ["InclusiveColorTools"]),
],
targets: [
.target(
name: "InclusiveColor",
dependencies: []),
.target(
name: "InclusiveColorTools",
dependencies: []),
.testTarget(
name: "InclusiveColorTests",
dependencies: ["InclusiveColor"]),
]
)
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# InclusiveColor **Beta**

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

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



## 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.



## Quick start

* 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
```

**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**
```
AssertInclusive(text: textColorArray,
backgrounds: bgColorArray,
fonts: .wcag2_body(),
inclusivity: .max99percent)
```
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.

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.

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.

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.

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\*`



## Feedback

Find a bug? Wish I wrote a better API? Suggestions on metrics or other features?

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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation
import XCTest

public extension XCTestCase {

/// 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
/// - 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.
/// - 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.
///
/// - Returns: Tuple: isComparable result, summary of failed comparison
///
@discardableResult
func AssertColorsComparable<C: ICAnyColor>(exp: C,
sut: C,
rgb888Tolerance: ICColorChannel = 1,
alpha8Tolerance: ICColorChannel = 0,
index: Int? = nil,
label: String? = nil,
file: String = #filePath,
line: Int = #line,
suppressFailure: Bool = false
) -> (isComparable: Bool, colorFails: String) {

AssertColorsComparable(exp: exp.sRGBA ?? ICSRGBA(.max),
sut: sut.sRGBA ?? ICSRGBA(.max),
rgb888Tolerance: rgb888Tolerance,
alpha8Tolerance: alpha8Tolerance,
index: index,
label: label,
file: file,
line: line,
suppressFailure: suppressFailure)
}
}
114 changes: 114 additions & 0 deletions Sources/InclusiveColor/Assertions/AssertColorsComparable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Foundation
import XCTest

public extension XCTestCase {

/// 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
/// - 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.
/// - 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.
///
/// - Returns: Tuple: isComparable result, summary of failed comparison
///
@discardableResult
func AssertColorsComparable(exp: ICSRGBA,
sut: ICSRGBA,
rgb888Tolerance: ICColorChannel = 1,
alpha8Tolerance: ICColorChannel = 0,
index: Int? = nil,
label: String? = nil,
file: String = #filePath,
line: Int = #line,
suppressFailure: Bool = false
) -> (isComparable: Bool, colorFails: String) {

// Compare

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 isAlphaEqual = abs(exp.a.distance(to: sut.a)) < toleranceA8

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

// Draft fail message

let _label = label ?? ""
let _index = index == nil ? "" : "\n[\(index!)] \(_label)"
let _exp = exp.descriptionComponentsRGB888
let _result = sut.descriptionComponentsRGB888
let _alpha = "A \(String(format: "%1.2f", exp.a))\(String(format: "%1.2f", sut.a))"
let message = ["\(_index) \(_exp.caption) -> \(_result.caption)",
red ? nil : "R \(_exp.r)\(_result.r)",
green ? nil : "G \(_exp.g)\(_result.g)",
blue ? nil : "B \(_exp.b)\(_result.b)",
isAlphaEqual ? nil : _alpha]
.compactMap { $0 }
.joined(separator: "\n")

// Report

if !suppressFailure {
ICFail(file, line, .unequal) { message }
}

return (false, message)
}


@discardableResult
func AssertColorsComparable(exp: sRGBColor,
sut: sRGBColor,
rgb888Tolerance: ICColorChannel = 1,
index: Int? = nil,
label: String? = nil,
file: String = #filePath,
line: Int = #line,
suppressFailure: Bool = false
) -> (isComparable: Bool, colorFails: String) {

// Compare

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

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

// Draft fail message

let _label = label ?? ""
let _index = index == nil ? "" : "\n[\(index!)] \(_label)"
let _exp = exp.descriptionComponentsRGB888
let _result = sut.descriptionComponentsRGB888
let message = ["\(_index) \(_exp.caption) -> \(_result.caption)",
red ? nil : "R \(_exp.r)\(_result.r)",
green ? nil : "G \(_exp.g)\(_result.g)",
blue ? nil : "B \(_exp.b)\(_result.b)"]
.compactMap { $0 }
.joined(separator: "\n")

// Report

if !suppressFailure {
ICFail(file, line, .unequal) { message }
}

return (false, message)
}


}
Loading

0 comments on commit 492a8e9

Please sign in to comment.