forked from lauraragone/Inclusive-Color
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
169 changed files
with
12,078 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
43 changes: 43 additions & 0 deletions
43
Sources/InclusiveColor/Assertions/AssertColorsComparable+AnyColor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
114
Sources/InclusiveColor/Assertions/AssertColorsComparable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
|
||
} |
Oops, something went wrong.