// Standalone tests for reasampler::theme — no REAPER, no LICE, no test framework. Same // fast assert loop as the sibling pure tests. // // The load-bearing test: EVERY text-on-surface pair the kit actually draws clears its WCAG // floor (AA 4.5:1 body / 3:1 large + state indicators) — the "punch to the floor, not past // it" rule made testable (brief §1, CONTEXT.md §palette). Plus: the direction constants are // the single point of change (structural — roleColor is the only color source), the // interaction-state transform behaves, the WCAG math is correct against known anchors, and // the Direction C spectral ramp interpolates its endpoints. #include "../src/theme.h" #include #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- WCAG math against known anchors ------------------------------------------ static void testContrastKnownAnchors() { const KitColor white{255, 255, 255, 255}; const KitColor black{0, 0, 0, 255}; // Black-on-white is the canonical 21:1. CHECK(std::fabs(contrastRatio(white, black) - 21.0) < 0.05); // Contrast is symmetric (order-independent). CHECK(std::fabs(contrastRatio(white, black) - contrastRatio(black, white)) < 1e-9); // A color against itself is 1:1 (no contrast). CHECK(std::fabs(contrastRatio(white, white) - 1.0) < 1e-9); // Luminance ordering: white brightest, black darkest. CHECK(relativeLuminance(white) > relativeLuminance(black)); CHECK(std::fabs(relativeLuminance(black)) < 1e-9); CHECK(std::fabs(relativeLuminance(white) - 1.0) < 1e-9); } // --- THE load-bearing test: every drawn text-on-surface pair clears its floor - // The surfaces text lands on (near-black elevation stack). static void testTextPrimaryClearsBodyFloorOnSurfaces() { const KitColor tp = roleColor(Role::TextPrimary); // Primary text is body text -> 4.5:1 on every surface it is drawn on. CHECK(contrastRatio(tp, roleColor(Role::BgBase)) >= textFloor(TextClass::Body)); CHECK(contrastRatio(tp, roleColor(Role::BgPanel)) >= textFloor(TextClass::Body)); CHECK(contrastRatio(tp, roleColor(Role::BgCell)) >= textFloor(TextClass::Body)); } static void testTextDimClearsItsFloorOnSurfaces() { const KitColor td = roleColor(Role::TextDim); // Dim/secondary text is used for units/counts (large-ish, low-emphasis) -> the // large/state floor 3:1 on the surfaces it appears on. Enforced from the vibrant side: // it must not be dimmed BELOW the floor. CHECK(contrastRatio(td, roleColor(Role::BgBase)) >= textFloor(TextClass::Large)); CHECK(contrastRatio(td, roleColor(Role::BgPanel)) >= textFloor(TextClass::Large)); CHECK(contrastRatio(td, roleColor(Role::BgCell)) >= textFloor(TextClass::Large)); } static void testAccentClearsStateFloorOnBackground() { // The accent is a UI-state indicator (selection border / active fill) -> 3:1 minimum // on the base background, "pushed to the floor from the vibrant side". CHECK(contrastRatio(roleColor(Role::Accent), roleColor(Role::BgBase)) >= textFloor(TextClass::Large)); // The hot (hover) accent is brighter still, so it also clears. CHECK(contrastRatio(roleColor(Role::AccentHot), roleColor(Role::BgBase)) >= textFloor(TextClass::Large)); } static void testWarnClearsStateFloorOnBackground() { // warn (destructive) must be unmistakable -> clears the state floor on the base. CHECK(contrastRatio(roleColor(Role::Warn), roleColor(Role::BgBase)) >= textFloor(TextClass::Large)); } // Label drawn on an ACTIVE (accent-filled) surface: the shell draws it in bg/base. That // pairing must also clear the body floor, or active buttons would be unreadable. static void testLabelOnActiveSurfaceClearsFloor() { const KitColor activeFill = roleColorState(Role::BgCell, InteractionState::Active); const KitColor labelOnActive = roleColor(Role::BgBase); // what drawButton uses CHECK(contrastRatio(labelOnActive, activeFill) >= textFloor(TextClass::Body)); } // --- Single point of change (structural guarantee) ---------------------------- // // roleColor is the ONLY color source; there is no other public accessor that yields a // palette color, so re-picking the direction is editing the one constants block roleColor // reads. We assert the roles are DISTINCT (the block actually differentiates them — a // collapsed/duplicated palette would betray a broken edit) and that the elevation stack // is monotonic (base darkest -> cell lightest), the invariant the direction must preserve. static void testRolesAreDistinctAndElevationMonotonic() { CHECK(!(roleColor(Role::BgBase) == roleColor(Role::BgPanel))); CHECK(!(roleColor(Role::BgPanel) == roleColor(Role::BgCell))); CHECK(!(roleColor(Role::TextPrimary) == roleColor(Role::TextDim))); CHECK(!(roleColor(Role::Accent) == roleColor(Role::Warn))); // Elevation reads as increasing luminance base < panel < cell. CHECK(relativeLuminance(roleColor(Role::BgBase)) < relativeLuminance(roleColor(Role::BgPanel))); CHECK(relativeLuminance(roleColor(Role::BgPanel)) < relativeLuminance(roleColor(Role::BgCell))); // Primary text is brighter than dim text (the type hierarchy). CHECK(relativeLuminance(roleColor(Role::TextPrimary)) > relativeLuminance(roleColor(Role::TextDim))); } // --- Interaction-state transform ---------------------------------------------- static void testStateRestIsIdentity() { for (Role r : {Role::BgBase, Role::BgPanel, Role::BgCell, Role::Accent}) { CHECK(roleColorState(r, InteractionState::Rest) == roleColor(r)); } } static void testHoverLightensPressedDarkens() { const KitColor rest = roleColor(Role::BgCell); const KitColor hover = roleColorState(Role::BgCell, InteractionState::Hover); const KitColor pressed = roleColorState(Role::BgCell, InteractionState::Pressed); // Hover lightens toward the hot accent; pressed darkens. CHECK(relativeLuminance(hover) > relativeLuminance(rest)); CHECK(relativeLuminance(pressed) < relativeLuminance(rest)); } static void testActiveIsAccent() { CHECK(roleColorState(Role::BgCell, InteractionState::Active) == roleColor(Role::Accent)); } static void testDisabledDropsAlphaAndDesaturates() { const KitColor rest = roleColor(Role::Accent); const KitColor dis = roleColorState(Role::Accent, InteractionState::Disabled); // Alpha drops to ~40%. CHECK(dis.a < rest.a); CHECK(dis.a >= 90 && dis.a <= 110); // 255 * 0.4 ~= 102 } // --- Direction C spectral ramp ------------------------------------------------ static void testSpectralInterpolatesEndpoints() { const KitColor lo = spectralColor(0.0); const KitColor hi = spectralColor(1.0); // Low is cool (blue-dominant), high is hot (red-dominant) — the identity of the ramp. CHECK(lo.b > lo.r); CHECK(hi.r > hi.b); // Midpoint sits between the endpoints on each channel. const KitColor mid = spectralColor(0.5); CHECK(mid.r > lo.r && mid.r < hi.r); // Clamps out of range. CHECK(spectralColor(-1.0) == lo); CHECK(spectralColor(2.0) == hi); } int main() { testContrastKnownAnchors(); testTextPrimaryClearsBodyFloorOnSurfaces(); testTextDimClearsItsFloorOnSurfaces(); testAccentClearsStateFloorOnBackground(); testWarnClearsStateFloorOnBackground(); testLabelOnActiveSurfaceClearsFloor(); testRolesAreDistinctAndElevationMonotonic(); testStateRestIsIdentity(); testHoverLightensPressedDarkens(); testActiveIsAccent(); testDisabledDropsAlphaAndDesaturates(); testSpectralInterpolatesEndpoints(); if (g_fail == 0) std::printf("theme: all tests passed\n"); else std::printf("theme: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }