1b4d0e67b7
Also corrects the cap-area, em-dash, glyph-overhang and heuristic-comment findings noted in review.
411 lines
23 KiB
C++
411 lines
23 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/core/ui/theme.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <initializer_list>
|
|
|
|
using namespace reasampler;
|
|
using namespace reasampler::ui;
|
|
|
|
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 (DS-2 revised REAPER-grey 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);
|
|
// DS-2 revised (grey re-read): text/dim is used for keybinding sub-labels / units on
|
|
// the mid-grey surfaces, and mid-grey-on-mid-grey is the classic AA failure. It is
|
|
// LOCKED to the lightest grey that still reads dim while clearing AA 4.5:1 BODY on the
|
|
// GREYEST surface it draws body text on (bg/cell). This is the load-bearing check: the
|
|
// spec-start #a0a0a0 is UNDER floor on grey by design — the value was lifted until this
|
|
// passes. Body floor on all three surfaces (bg/cell is the tight one).
|
|
CHECK(contrastRatio(td, roleColor(Role::BgBase)) >= textFloor(TextClass::Body));
|
|
CHECK(contrastRatio(td, roleColor(Role::BgPanel)) >= textFloor(TextClass::Body));
|
|
CHECK(contrastRatio(td, roleColor(Role::BgCell)) >= textFloor(TextClass::Body));
|
|
}
|
|
|
|
static void testAccentsClearIndicatorFloorOnCell() {
|
|
// Each accent is used as a state indicator / active fill on bg/cell (selection border,
|
|
// active segment, active tab). The move to REAPER grey shrank the cushion from ~15:1 on
|
|
// near-black to 3.9:1 (secondary) - 8.7:1 (hot), so the floor is verified on bg/cell, NOT
|
|
// the old near-black. If a pastel drops below, nudge that hue deeper; hue stays
|
|
// lime/teal/purple.
|
|
for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary,
|
|
Role::AccentHot}) {
|
|
CHECK(contrastRatio(roleColor(a), roleColor(Role::BgCell))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
// The primary accent also leads the mode-switch/tab active fill drawn over bg/base.
|
|
CHECK(contrastRatio(roleColor(Role::AccentPrimary), roleColor(Role::BgBase))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
|
|
// The docked panel's region-title band: secondary/tertiary as the title itself, primary as the
|
|
// "Active:" readout beside it, all on bg/panel. The titles clear the 3:1 indicator floor and
|
|
// NOT the 4.5:1 body floor (secondary sits at 4.38:1), so they are drawn in Font::RegionTitle —
|
|
// 19px bold, which is what ENTITLES them to the large-text floor. draw_kit.cpp static_asserts
|
|
// those metrics against theme's thresholds; that assert and this pair are one guard in two
|
|
// halves, and removing either re-opens the AA failure that 15px semibold hid.
|
|
static void testRegionTitleAccentsClearLargeFloorOnPanel() {
|
|
for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary}) {
|
|
CHECK(contrastRatio(roleColor(a), roleColor(Role::BgPanel))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
// Tighter pin at the measured value, so drift is caught before it reaches the floor.
|
|
CHECK(contrastRatio(roleColor(Role::AccentSecondary), roleColor(Role::BgPanel)) >= 4.3);
|
|
}
|
|
|
|
// Text drawn ON a pastel accent fill (retained tight pair, re-verified for the pastels):
|
|
// active buttons/segments/tabs fill with the primary accent and draw their label in
|
|
// bg/base (near-black-ish grey). That near-black-on-pastel pair must clear AA 4.5:1 body,
|
|
// or active controls would be unreadable. Verified for EACH pastel that can be a text fill.
|
|
static void testTextOnPastelFillClearsBodyFloor() {
|
|
const KitColor label = roleColor(Role::BgBase); // what drawButton/drawListRow use
|
|
for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary}) {
|
|
CHECK(contrastRatio(label, roleColor(a)) >= textFloor(TextClass::Body));
|
|
}
|
|
}
|
|
|
|
// Secondary and tertiary are the CATEGORICAL pair (region titles, spectral bands). They
|
|
// must read as DISTINCT categories, not two greys. Two pastels can be near-equal in
|
|
// LUMINANCE yet clearly distinct in HUE (teal vs purple differ mainly by channel balance,
|
|
// not brightness) — so distinguishability is a hue-separation check, not a luminance-
|
|
// contrast one. Assert a meaningful per-channel difference: teal is green/blue-leaning,
|
|
// purple is red/blue-leaning, so their channel BALANCE diverges substantially.
|
|
static void testSecondaryTertiaryAreDistinguishable() {
|
|
const KitColor sec = roleColor(Role::AccentSecondary); // teal: G,B high, R low
|
|
const KitColor ter = roleColor(Role::AccentTertiary); // purple: R,B high, G lower
|
|
CHECK(!(sec == ter));
|
|
CHECK(!(roleColor(Role::AccentPrimary) == sec));
|
|
// Hue divergence: teal's green dominates its red; purple's red dominates its green.
|
|
// A muddying that collapsed them toward a common grey would break this ordering.
|
|
CHECK(sec.g > sec.r); // teal leans green over red
|
|
CHECK(ter.r > ter.g); // purple leans red over green
|
|
// And the total channel-balance gap is large (sum of absolute channel deltas), so the
|
|
// two are far apart in color space even though close in luminance.
|
|
const int delta = std::abs(int(sec.r) - int(ter.r)) +
|
|
std::abs(int(sec.g) - int(ter.g)) +
|
|
std::abs(int(sec.b) - int(ter.b));
|
|
CHECK(delta >= 60);
|
|
}
|
|
|
|
// The envelope overlay is traced OVER the waveform, which draws in the primary accent — an
|
|
// accent-on-accent pair no surface floor covers, since neither is a surface. Every categorical
|
|
// accent is light enough to sit under 2:1 against the lime, so pointing the trace back at one
|
|
// fails here.
|
|
static void testOverlayTraceClearsIndicatorFloorOnTheWaveform() {
|
|
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::AccentPrimary))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
|
|
// The trace's own surface floor, on the bg/base the waveform band fills with. This pair does
|
|
// double duty: the GRABBED envelope handle is an overlay/trace ring around a bg/base core, so
|
|
// the same number is also that mark's internal ring-vs-core contrast.
|
|
static void testOverlayTraceClearsFloorOnItsDrawSurface() {
|
|
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::BgBase))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
|
|
// The velocity-curve trace is drawn in accent/secondary over a HOVER-lightened bg/cell (the
|
|
// chrome band's mini preview button). Hover lightens the surface toward accent/hot, closing on
|
|
// the deep teal from below: 3.03:1 against a 3:1 floor — the thinnest margin in the tree, and
|
|
// the true binding limiter on how dark accent/secondary may go. No rest-surface check sees it.
|
|
static void testCurveTraceOnHoverSurfaceClearsFloor() {
|
|
CHECK(contrastRatio(roleColor(Role::AccentSecondary),
|
|
roleColorState(Role::BgCell, InteractionState::Hover))
|
|
>= textFloor(TextClass::Large));
|
|
}
|
|
|
|
// The trace's OTHER two neighbours in the waveform rect, both composited or drawn beneath it.
|
|
// Enumerating only bg/base under-counted them.
|
|
// - The loop-span fill: accent/secondary at kLoopSpanFillAlpha over bg/base. The trace over
|
|
// that composite is 2.25:1 — a KNOWN, ACCEPTED under-floor pair, pinned here as a RANGE so
|
|
// it is recorded rather than silently believed to clear. The two-neighbour ceiling means no
|
|
// trace value fixes it (clearing the fill breaks the lime or bg/base pair); if it is ever
|
|
// resolved, the FILL is what changes, and this assertion is what will fail first.
|
|
// - drawWaveform's line/hairline zero line, at 1.92:1. Also under floor, and also unfixable
|
|
// from the trace's side.
|
|
static void testOverlayTraceAgainstItsRemainingNeighbours() {
|
|
const KitColor loopFill = compositeOver(roleColor(Role::AccentSecondary),
|
|
roleColor(Role::BgBase), kLoopSpanFillAlpha);
|
|
const double onFill = contrastRatio(roleColor(Role::OverlayTrace), loopFill);
|
|
CHECK(onFill >= 2.2 && onFill < textFloor(TextClass::Large));
|
|
const double onZeroLine = contrastRatio(roleColor(Role::OverlayTrace),
|
|
roleColor(Role::LineHairline));
|
|
CHECK(onZeroLine >= 1.9 && onZeroLine < textFloor(TextClass::Large));
|
|
}
|
|
|
|
// compositeOver is the arithmetic the pairs above depend on, so anchor it: full alpha is the
|
|
// overlay, zero alpha is the surface, and the composite of a lighter color over a darker one
|
|
// lands strictly between the two.
|
|
static void testCompositeOverAnchors() {
|
|
const KitColor over = roleColor(Role::AccentSecondary);
|
|
const KitColor under = roleColor(Role::BgBase);
|
|
CHECK(compositeOver(over, under, 1.0) == over);
|
|
CHECK(compositeOver(over, under, 0.0) == under);
|
|
CHECK(compositeOver(over, under, 5.0) == over); // alpha clamps
|
|
CHECK(compositeOver(over, under, -1.0) == under);
|
|
const double mid = relativeLuminance(compositeOver(over, under, 0.5));
|
|
CHECK(mid > relativeLuminance(under) && mid < relativeLuminance(over));
|
|
}
|
|
|
|
// editor_paint_waveform.cpp draws the loop span/markers and the envelope overlay trace into
|
|
// the SAME overlay rect, so they must read as two things. Luminance cannot carry that — each is
|
|
// pinned near its own floor — so the separation is a hue one: teal leans green over red, the
|
|
// trace violet leans red over green. A palette edit collapsing them onto one role fails here.
|
|
static void testLoopMarkerAndOverlayTraceAreDistinct() {
|
|
const KitColor loopMarker = roleColor(Role::AccentSecondary);
|
|
const KitColor trace = roleColor(Role::OverlayTrace);
|
|
CHECK(!(loopMarker == trace));
|
|
CHECK(loopMarker.g > loopMarker.r);
|
|
CHECK(trace.r > trace.g);
|
|
const int delta = std::abs(int(loopMarker.r) - int(trace.r)) +
|
|
std::abs(int(loopMarker.g) - int(trace.g)) +
|
|
std::abs(int(loopMarker.b) - int(trace.b));
|
|
CHECK(delta >= 60);
|
|
}
|
|
|
|
// AccentSecondary was darkened from a pastel teal (#84D6D0) to a deep teal (#38A8A0, same
|
|
// hue/saturation, lower lightness). Locks the re-measured numbers so a future nudge that erodes
|
|
// past either the 3:1 indicator floor or the AA 4.5:1 text-on-fill floor fails the build. The
|
|
// binding limiter on going darker is NOT one of these — it is the hover-surface pair above.
|
|
static void testAccentSecondaryDarkerTealClearsFloors() {
|
|
const KitColor sec = roleColor(Role::AccentSecondary);
|
|
CHECK(contrastRatio(sec, roleColor(Role::BgBase)) >= textFloor(TextClass::Large));
|
|
CHECK(contrastRatio(sec, roleColor(Role::BgPanel)) >= textFloor(TextClass::Large));
|
|
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= textFloor(TextClass::Large));
|
|
// Tighter pins at the measured values: a regression that darkens secondary further would
|
|
// clear these before it clears the nominal 3:1/4.5:1 floors above, catching the drift early.
|
|
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= 3.9);
|
|
CHECK(contrastRatio(roleColor(Role::BgBase), sec) >= 4.9);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
// Text drawn on a HOVER-state surface (Phase L, L3): the VST editor + embed restyle lights
|
|
// hover on inactive segments/tabs/cards by lightening bg/cell toward accent/hot (~10%).
|
|
// Lightening the surface REDUCES contrast against light text, so re-verify the pairs on the
|
|
// hover surface, not just on rest bg/cell. (A distinct drawn pair the L3 shells introduce,
|
|
// not covered by the rest-surface tests above.)
|
|
// - text/PRIMARY (card name, segment/tab labels) is BODY -> must clear AA 4.5:1 even hovered.
|
|
// - text/DIM is used ONLY as a compact UI LABEL on a hoverable surface (the card's root/key
|
|
// BADGE — the one dim-on-hover pair the restyle draws), so it clears at the 3:1 large/
|
|
// UI-label floor, per §2.1's "confine dim to large/UI-label use where the 3:1 floor
|
|
// applies." A lightened surface pulls a mid-grey dim toward the body floor; the badge is
|
|
// deliberately a label, not body, so 3:1 is the correct floor and this locks it there.
|
|
static void testTextOnHoverSurfaceClearsFloor() {
|
|
const KitColor hover = roleColorState(Role::BgCell, InteractionState::Hover);
|
|
CHECK(contrastRatio(roleColor(Role::TextPrimary), hover) >= textFloor(TextClass::Body));
|
|
CHECK(contrastRatio(roleColor(Role::TextDim), hover) >= textFloor(TextClass::Large));
|
|
}
|
|
|
|
// The card name strip (panel_render.cpp's drawCardName) draws text/primary Micro (10px, BODY
|
|
// class) over a bg/base scrim at kCardNameScrimAlpha, itself drawn over whatever the waveform
|
|
// left in that rect. Enumerate both backgrounds a loud/quiet capture can leave there: the
|
|
// accent-lime fill (a full-scale peak reaching the strip) and bare bg/cell (a quiet capture,
|
|
// nothing painted that high). Unscrimmed, text/primary reads ~1:1 against the fill — this is
|
|
// the load-bearing check that the scrim actually fixes it.
|
|
static void testCardNameScrimClearsBodyFloorOnItsWorstBackground() {
|
|
const KitColor scrim = roleColor(Role::BgBase);
|
|
const KitColor onFill = compositeOver(scrim, roleColor(Role::AccentPrimary),
|
|
kCardNameScrimAlpha);
|
|
const KitColor onCell = compositeOver(scrim, roleColor(Role::BgCell), kCardNameScrimAlpha);
|
|
CHECK(contrastRatio(roleColor(Role::TextPrimary), onFill) >= textFloor(TextClass::Body));
|
|
CHECK(contrastRatio(roleColor(Role::TextPrimary), onCell) >= textFloor(TextClass::Body));
|
|
// Without the scrim, text/primary on the bare accent-lime fill is UNDER floor — pins the
|
|
// defect the scrim exists to close, so a future removal of the scrim fails this first.
|
|
CHECK(contrastRatio(roleColor(Role::TextPrimary), roleColor(Role::AccentPrimary))
|
|
< textFloor(TextClass::Body));
|
|
}
|
|
|
|
// The waveform band's state caption and its four mark labels (editor_paint_waveform.cpp) draw
|
|
// text/dim (the caption, the non-promoted labels) and text/primary (the promoted label) — both
|
|
// Font::Micro, both body class — over a bg/base scrim at kWaveformLabelScrimAlpha, itself drawn
|
|
// over whatever drawEnvelope left in that rect. Unlike the card name strip, the worst case here
|
|
// is text/DIM, not text/primary, so the alpha is higher than kCardNameScrimAlpha's — pin both
|
|
// roles against the worst background (a full-scale envelope peak, accent/primary) and pin the
|
|
// defect the scrim exists to close.
|
|
static void testWaveformLabelScrimClearsBodyFloorOnItsWorstBackground() {
|
|
const KitColor scrim = roleColor(Role::BgBase);
|
|
const KitColor onFill = compositeOver(scrim, roleColor(Role::AccentPrimary),
|
|
kWaveformLabelScrimAlpha);
|
|
CHECK(contrastRatio(roleColor(Role::TextDim), onFill) >= textFloor(TextClass::Body));
|
|
CHECK(contrastRatio(roleColor(Role::TextPrimary), onFill) >= textFloor(TextClass::Body));
|
|
// Without the scrim, text/dim on the bare accent-lime fill is UNDER floor (~1.58:1) — pins
|
|
// the defect the scrim exists to close, so a future removal of the scrim fails this first.
|
|
CHECK(contrastRatio(roleColor(Role::TextDim), roleColor(Role::AccentPrimary))
|
|
< 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::AccentPrimary) == 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::AccentPrimary}) {
|
|
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 testActiveIsPrimaryAccent() {
|
|
// DS-2 revised: the Active state is ALWAYS the primary accent ("what's live" leads).
|
|
CHECK(roleColorState(Role::BgCell, InteractionState::Active)
|
|
== roleColor(Role::AccentPrimary));
|
|
}
|
|
|
|
static void testDisabledDropsAlphaAndDesaturates() {
|
|
const KitColor rest = roleColor(Role::AccentPrimary);
|
|
const KitColor dis = roleColorState(Role::AccentPrimary, 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 testSpectralIsPastelRampAnchoredOnAccents() {
|
|
// DS-2 revised Direction C: the spectral ramp is a PASTEL sweep — lime (low) -> teal (mid)
|
|
// -> purple (high) — NOT the old neon cool->hot.
|
|
const KitColor lo = spectralColor(0.0);
|
|
const KitColor mid = spectralColor(0.5);
|
|
const KitColor hi = spectralColor(1.0);
|
|
// The endpoints still ARE the primary/tertiary accent constants. The mid stop is NOT the
|
|
// secondary accent and must not be re-aliased to it — see the monotonicity test below.
|
|
CHECK(lo == roleColor(Role::AccentPrimary)); // low = pastel lime
|
|
CHECK(!(mid == roleColor(Role::AccentSecondary)));
|
|
CHECK(hi == roleColor(Role::AccentTertiary)); // high = pastel purple
|
|
// Low is lime (green-dominant); high is purple (red+blue over green) — distinct hues.
|
|
CHECK(lo.g > lo.r && lo.g > lo.b);
|
|
CHECK(hi.b > hi.g && hi.r > hi.g);
|
|
// A quarter-point interpolates within the lime->teal half (not off the family).
|
|
const KitColor q = spectralColor(0.25);
|
|
CHECK(q.b > lo.b && q.b < mid.b); // blue rises lime -> teal
|
|
// Clamps out of range to the endpoints.
|
|
CHECK(spectralColor(-1.0) == lo);
|
|
CHECK(spectralColor(2.0) == hi);
|
|
}
|
|
|
|
// The strip's stops must fall in ONE direction, or position along it stops meaning anything.
|
|
// Nothing caught this when the mid stop aliased a categorical accent that was later darkened
|
|
// below the high stop, inverting lo->mid->hi; that is the break this guards.
|
|
static void testSpectralRampLuminanceIsMonotonic() {
|
|
const double lo = relativeLuminance(spectralColor(0.0));
|
|
const double mid = relativeLuminance(spectralColor(0.5));
|
|
const double hi = relativeLuminance(spectralColor(1.0));
|
|
CHECK(lo > mid);
|
|
CHECK(mid > hi);
|
|
}
|
|
|
|
int main() {
|
|
testContrastKnownAnchors();
|
|
testTextPrimaryClearsBodyFloorOnSurfaces();
|
|
testTextDimClearsItsFloorOnSurfaces();
|
|
testAccentsClearIndicatorFloorOnCell();
|
|
testRegionTitleAccentsClearLargeFloorOnPanel();
|
|
testTextOnPastelFillClearsBodyFloor();
|
|
testTextOnHoverSurfaceClearsFloor();
|
|
testCardNameScrimClearsBodyFloorOnItsWorstBackground();
|
|
testWaveformLabelScrimClearsBodyFloorOnItsWorstBackground();
|
|
testCurveTraceOnHoverSurfaceClearsFloor();
|
|
testSecondaryTertiaryAreDistinguishable();
|
|
testOverlayTraceClearsIndicatorFloorOnTheWaveform();
|
|
testOverlayTraceClearsFloorOnItsDrawSurface();
|
|
testOverlayTraceAgainstItsRemainingNeighbours();
|
|
testCompositeOverAnchors();
|
|
testLoopMarkerAndOverlayTraceAreDistinct();
|
|
testAccentSecondaryDarkerTealClearsFloors();
|
|
testWarnClearsStateFloorOnBackground();
|
|
testLabelOnActiveSurfaceClearsFloor();
|
|
testRolesAreDistinctAndElevationMonotonic();
|
|
testStateRestIsIdentity();
|
|
testHoverLightensPressedDarkens();
|
|
testActiveIsPrimaryAccent();
|
|
testDisabledDropsAlphaAndDesaturates();
|
|
testSpectralIsPastelRampAnchoredOnAccents();
|
|
testSpectralRampLuminanceIsMonotonic();
|
|
|
|
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;
|
|
}
|