170 lines
7.8 KiB
C++
170 lines
7.8 KiB
C++
// 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 <cmath>
|
|
#include <cstdio>
|
|
#include <initializer_list>
|
|
|
|
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;
|
|
}
|