diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 2fcd2b4..5f6cee3 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/AccessibilityBaseline.h b/editor/src/AccessibilityBaseline.h new file mode 100644 index 0000000..c89ebb2 --- /dev/null +++ b/editor/src/AccessibilityBaseline.h @@ -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 +#include +#include +#include +#include + +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 failures; + std::vector traversalOrder; +}; + +class AccessibilityBaseline { +public: + static std::vector keyboardTraversalOrder( + const std::vector& nodes) { + std::vector 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 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& 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); + } +}; diff --git a/editor/tests/step513_test.cpp b/editor/tests/step513_test.cpp new file mode 100644 index 0000000..68be739 --- /dev/null +++ b/editor/tests/step513_test.cpp @@ -0,0 +1,138 @@ +// Step 513: Accessibility Baseline Pass (12 tests) + +#include "AccessibilityBaseline.h" + +#include + +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 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; +} diff --git a/progress.md b/progress.md index c524882..d8ffe79 100644 --- a/progress.md +++ b/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`