Step 513: add accessibility baseline traversal and contrast checks
This commit is contained in:
@@ -3496,4 +3496,13 @@ target_link_libraries(step512_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step513_test tests/step513_test.cpp)
|
||||
target_include_directories(step513_test PRIVATE src)
|
||||
target_link_libraries(step513_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
99
editor/src/AccessibilityBaseline.h
Normal file
99
editor/src/AccessibilityBaseline.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
// Step 513: Accessibility Baseline Pass
|
||||
// Keyboard traversal, focus-ring visibility, and contrast checks for baseline
|
||||
// accessibility requirements in a headless validation model.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
struct AccessibilityNode {
|
||||
std::string id;
|
||||
bool focusable = true;
|
||||
bool visible = true;
|
||||
bool enabled = true;
|
||||
int tabOrder = 0;
|
||||
};
|
||||
|
||||
struct FocusRingStyle {
|
||||
float thickness = 0.0f;
|
||||
float alpha = 0.0f;
|
||||
float contrast = 1.0f;
|
||||
};
|
||||
|
||||
struct AccessibilityReport {
|
||||
bool pass = true;
|
||||
std::vector<std::string> failures;
|
||||
std::vector<std::string> traversalOrder;
|
||||
};
|
||||
|
||||
class AccessibilityBaseline {
|
||||
public:
|
||||
static std::vector<std::string> keyboardTraversalOrder(
|
||||
const std::vector<AccessibilityNode>& nodes) {
|
||||
std::vector<AccessibilityNode> filtered;
|
||||
for (const auto& n : nodes) {
|
||||
if (n.visible && n.enabled && n.focusable) filtered.push_back(n);
|
||||
}
|
||||
std::sort(filtered.begin(), filtered.end(),
|
||||
[](const auto& a, const auto& b) { return a.tabOrder < b.tabOrder; });
|
||||
std::vector<std::string> out;
|
||||
for (const auto& n : filtered) out.push_back(n.id);
|
||||
return out;
|
||||
}
|
||||
|
||||
static FocusRingStyle resolveFocusRing(bool focused,
|
||||
bool highContrast,
|
||||
float bgLuminance,
|
||||
float ringLuminance) {
|
||||
FocusRingStyle s;
|
||||
if (!focused) return s;
|
||||
s.thickness = highContrast ? 3.0f : 2.0f;
|
||||
s.alpha = highContrast ? 1.0f : 0.9f;
|
||||
s.contrast = contrast(bgLuminance, ringLuminance);
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool focusRingVisible(const FocusRingStyle& s) {
|
||||
return s.thickness >= 2.0f && s.alpha >= 0.85f && s.contrast >= 3.0f;
|
||||
}
|
||||
|
||||
static AccessibilityReport validate(const std::vector<AccessibilityNode>& nodes,
|
||||
const FocusRingStyle& ring,
|
||||
float textBgContrast,
|
||||
float iconBgContrast) {
|
||||
AccessibilityReport r;
|
||||
r.traversalOrder = keyboardTraversalOrder(nodes);
|
||||
|
||||
if (r.traversalOrder.empty()) {
|
||||
r.pass = false;
|
||||
r.failures.push_back("no-focusable-controls");
|
||||
}
|
||||
|
||||
if (!focusRingVisible(ring)) {
|
||||
r.pass = false;
|
||||
r.failures.push_back("focus-ring-not-visible");
|
||||
}
|
||||
|
||||
if (textBgContrast < 4.5f) {
|
||||
r.pass = false;
|
||||
r.failures.push_back("text-contrast-low");
|
||||
}
|
||||
|
||||
if (iconBgContrast < 3.0f) {
|
||||
r.pass = false;
|
||||
r.failures.push_back("icon-contrast-low");
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
private:
|
||||
static float contrast(float a, float b) {
|
||||
float l1 = std::max(a, b);
|
||||
float l2 = std::min(a, b);
|
||||
return (l1 + 0.05f) / (l2 + 0.05f);
|
||||
}
|
||||
};
|
||||
138
editor/tests/step513_test.cpp
Normal file
138
editor/tests/step513_test.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
// Step 513: Accessibility Baseline Pass (12 tests)
|
||||
|
||||
#include "AccessibilityBaseline.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static std::vector<AccessibilityNode> baseNodes() {
|
||||
return {
|
||||
{"menu", true, true, true, 0},
|
||||
{"explorer", true, true, true, 1},
|
||||
{"editor", true, true, true, 2},
|
||||
{"status", true, true, true, 3}
|
||||
};
|
||||
}
|
||||
|
||||
void test_traversal_order_uses_tab_order() {
|
||||
TEST(traversal_order_uses_tab_order);
|
||||
auto order = AccessibilityBaseline::keyboardTraversalOrder(baseNodes());
|
||||
CHECK(order.size() == 4, "expected 4 items");
|
||||
CHECK(order[0] == "menu" && order[3] == "status", "unexpected order");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_traversal_skips_disabled_nodes() {
|
||||
TEST(traversal_skips_disabled_nodes);
|
||||
auto nodes = baseNodes();
|
||||
nodes[1].enabled = false;
|
||||
auto order = AccessibilityBaseline::keyboardTraversalOrder(nodes);
|
||||
CHECK(order.size() == 3, "disabled node should be skipped");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_traversal_skips_invisible_nodes() {
|
||||
TEST(traversal_skips_invisible_nodes);
|
||||
auto nodes = baseNodes();
|
||||
nodes[2].visible = false;
|
||||
auto order = AccessibilityBaseline::keyboardTraversalOrder(nodes);
|
||||
CHECK(order.size() == 3, "invisible node should be skipped");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_focus_ring_resolves_for_focused_control() {
|
||||
TEST(focus_ring_resolves_for_focused_control);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, false, 0.1f, 0.9f);
|
||||
CHECK(ring.thickness >= 2.0f, "ring thickness too low");
|
||||
CHECK(ring.alpha >= 0.85f, "ring alpha too low");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_focus_ring_disabled_when_not_focused() {
|
||||
TEST(focus_ring_disabled_when_not_focused);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(false, false, 0.1f, 0.9f);
|
||||
CHECK(ring.thickness == 0.0f, "unfocused ring should be off");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_focus_ring_visible_with_good_contrast() {
|
||||
TEST(focus_ring_visible_with_good_contrast);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, true, 0.1f, 0.9f);
|
||||
CHECK(AccessibilityBaseline::focusRingVisible(ring), "ring should be visible");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_focus_ring_not_visible_with_low_contrast() {
|
||||
TEST(focus_ring_not_visible_with_low_contrast);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, false, 0.40f, 0.42f);
|
||||
CHECK(!AccessibilityBaseline::focusRingVisible(ring), "ring should fail visibility");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_passes_for_accessible_baseline() {
|
||||
TEST(validate_passes_for_accessible_baseline);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, true, 0.1f, 0.9f);
|
||||
auto report = AccessibilityBaseline::validate(baseNodes(), ring, 7.0f, 4.0f);
|
||||
CHECK(report.pass, "baseline should pass");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_fails_when_no_focusable_controls() {
|
||||
TEST(validate_fails_when_no_focusable_controls);
|
||||
auto nodes = baseNodes();
|
||||
for (auto& n : nodes) n.focusable = false;
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, true, 0.1f, 0.9f);
|
||||
auto report = AccessibilityBaseline::validate(nodes, ring, 7.0f, 4.0f);
|
||||
CHECK(!report.pass, "expected failure");
|
||||
CHECK(!report.failures.empty(), "expected failure reasons");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_fails_text_contrast_below_aa() {
|
||||
TEST(validate_fails_text_contrast_below_aa);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, true, 0.1f, 0.9f);
|
||||
auto report = AccessibilityBaseline::validate(baseNodes(), ring, 3.0f, 4.0f);
|
||||
CHECK(!report.pass, "should fail text contrast");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_fails_icon_contrast_below_threshold() {
|
||||
TEST(validate_fails_icon_contrast_below_threshold);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, true, 0.1f, 0.9f);
|
||||
auto report = AccessibilityBaseline::validate(baseNodes(), ring, 7.0f, 2.0f);
|
||||
CHECK(!report.pass, "should fail icon contrast");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_fails_when_focus_ring_not_visible() {
|
||||
TEST(validate_fails_when_focus_ring_not_visible);
|
||||
auto ring = AccessibilityBaseline::resolveFocusRing(true, false, 0.4f, 0.42f);
|
||||
auto report = AccessibilityBaseline::validate(baseNodes(), ring, 7.0f, 4.0f);
|
||||
CHECK(!report.pass, "should fail hidden focus ring");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 513: Accessibility Baseline Pass\n";
|
||||
|
||||
test_traversal_order_uses_tab_order(); // 1
|
||||
test_traversal_skips_disabled_nodes(); // 2
|
||||
test_traversal_skips_invisible_nodes(); // 3
|
||||
test_focus_ring_resolves_for_focused_control(); // 4
|
||||
test_focus_ring_disabled_when_not_focused(); // 5
|
||||
test_focus_ring_visible_with_good_contrast(); // 6
|
||||
test_focus_ring_not_visible_with_low_contrast(); // 7
|
||||
test_validate_passes_for_accessible_baseline(); // 8
|
||||
test_validate_fails_when_no_focusable_controls(); // 9
|
||||
test_validate_fails_text_contrast_below_aa(); // 10
|
||||
test_validate_fails_icon_contrast_below_threshold(); // 11
|
||||
test_validate_fails_when_focus_ring_not_visible(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
34
progress.md
34
progress.md
@@ -8145,3 +8145,37 @@ validation of layering invariants and interaction blocking behavior.
|
||||
- `editor/src/WindowHierarchyModel.h` within header-size limit (`142` <= `600`)
|
||||
- `editor/tests/step512_test.cpp` within test-file size guidance (`153` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 513: Accessibility Baseline Pass
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements a baseline accessibility validation model covering keyboard-only
|
||||
traversal, focus-ring visibility constraints, and contrast thresholds for text
|
||||
and iconography.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/AccessibilityBaseline.h` - accessibility baseline module:
|
||||
- keyboard traversal order generation from tab-order metadata
|
||||
- focus-ring style resolution with high-contrast mode support
|
||||
- focus-ring visibility checks (thickness/alpha/contrast)
|
||||
- aggregate accessibility validation (focusability, ring, text/icon contrast)
|
||||
- `editor/tests/step513_test.cpp` - 12 tests covering:
|
||||
- traversal ordering and disabled/invisible-node filtering
|
||||
- focus-ring enabled/disabled and visibility semantics
|
||||
- positive baseline pass condition
|
||||
- failure-path detection for no focus targets, low text contrast,
|
||||
low icon contrast, and non-visible focus ring
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step513_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step513_test` - PASS
|
||||
- `./editor/build-native/step513_test` - PASS (12/12)
|
||||
- `./editor/build-native/step512_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/AccessibilityBaseline.h` within header-size limit (`99` <= `600`)
|
||||
- `editor/tests/step513_test.cpp` within test-file size guidance (`138` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user