Files
reasampler/tests/test_footer_bar.cpp
T
daniel f2cdf676f3 Ψ-W1-T2 review remediation: solo restore drops on visible-in-target, not parked; N-mode segments read dead when unroutable
Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
2026-08-01 20:13:10 -04:00

285 lines
14 KiB
C++

// Standalone tests for reasampler::footer_bar — no REAPER, no test framework. Same fast loop
// as the sibling pure tests (prune_button / mode_switch / action_bar): assert the L4 footer
// LEFT-group layout (toggle . count . Tail) and hit-testing directly.
//
// Covers (L4 brief §footer test cases):
// * Layout: toggle + count + Tail placed left-to-right in the specified order without
// overlap, starting at leftPad, vertically inset; the narrow toggle sits at the LEFT.
// * The group stays clear of the reserved RIGHT region (prune + version) — nothing crosses
// footer.right - rightReserve.
// * Suppression: a too-narrow footer drops later affordances (Tail first, then count),
// the toggle surviving longest; a degenerate footer yields an all-empty layout.
// * countWidth <= 0 hides the count label and the Tail sits right after the toggle.
// * Hit-test: Toggle / Tail returned for in-bounds points, None outside AND on the count
// label (a passive readout, never a control); half-open bounds; suppressed box claims none.
#include "../src/core/ui/footer_bar.h"
#include <cstdio>
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)
// A spec with round numbers so expected pixels are hand-checkable.
static FooterBarSpec roundSpec() {
FooterBarSpec s;
s.toggleWidth = 130;
s.countWidth = 60;
s.tailWidth = 130;
s.gap = 6;
s.leftPad = 8;
s.verticalInset = 4;
s.rightReserve = 168;
return s;
}
// --- Layout: wide footer, all three placed, left-to-right ---------------------
// A wide footer fits all three. Verify order, gaps, no overlap, vertical inset, and that the
// Tail (last) stays clear of the reserved right region.
static void testWideFooterAllPlaced() {
const FooterBarSpec s = roundSpec();
// Need room for toggle(130)+gap(6)+count(60)+gap(6)+tail(130) = 332 from leftPad(8) => 340,
// plus rightReserve(168). Width 700 is comfortable.
FooterRect f{0, 100, 700, 26};
const FooterBarLayout L = computeFooterBar(f, s);
const int top = 100 + 4, boxH = 26 - 8; // 104, 18
CHECK((L.toggle == FooterBarRect{8, top, 130, boxH}));
CHECK((L.count == FooterBarRect{8 + 130 + 6, top, 60, boxH})); // x = 144
CHECK((L.tail == FooterBarRect{8 + 130 + 6 + 60 + 6, top, 130, boxH})); // x = 210
// Order + no overlap: each starts at-or-after the previous end.
CHECK(L.count.x >= L.toggle.x + L.toggle.width);
CHECK(L.tail.x >= L.count.x + L.count.width);
// The whole group clears the reserved right region.
CHECK(L.tail.x + L.tail.width <= f.x + f.width - s.rightReserve);
}
// Origin offset honoured: the group anchors to THIS footer's left, not 0.
static void testOffsetFooterAnchorsLeft() {
const FooterBarSpec s = roundSpec();
FooterRect f{20, 200, 700, 26};
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(L.toggle.x == 20 + 8);
CHECK(!L.toggle.empty() && !L.count.empty() && !L.tail.empty());
}
// --- Suppression: narrowing drops later affordances first ---------------------
// A footer wide enough for toggle + count but not the Tail: Tail suppressed, toggle+count stay.
static void testTailSuppressedWhenTight() {
const FooterBarSpec s = roundSpec();
// Right bound = width - 168. Toggle ends at 8+130=138; count ends at 144+60=204.
// Tail would start at 210 and end at 340; need 340 <= width-168 => width >= 508 for Tail.
// count needs 204 <= width-168 => width >= 372. Pick width 480: count fits, Tail does not.
FooterRect f{0, 0, 480, 26};
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(!L.toggle.empty());
CHECK(!L.count.empty());
CHECK(L.tail.empty());
}
// Narrower still: only the toggle survives (count and Tail both drop). When the count does not
// fit, the Tail is offered the count's slot — but if the Tail also does not fit there, both are
// empty and only the toggle remains.
static void testOnlyToggleSurvives() {
const FooterBarSpec s = roundSpec();
// Toggle needs 138 <= width-168 => width >= 306. count needs width >= 372.
// At the count's slot (x=144) the Tail (130) would end at 274; needs 274 <= width-168 =>
// width >= 442. Pick width 330: toggle fits (306), count does not (372), Tail-at-count-slot
// does not (442) -> only toggle.
FooterRect f{0, 0, 330, 26};
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(!L.toggle.empty());
CHECK(L.count.empty());
CHECK(L.tail.empty());
}
// When the count does NOT fit but the Tail WOULD fit at the count's slot, the Tail takes it
// (a passive label yields to the interactive button). Right after the toggle, no count gap.
static void testTailTakesCountSlotWhenCountDrops() {
const FooterBarSpec s = roundSpec();
// Want: count does not fit, Tail-at-count-slot fits. count fits at width >= 372.
// Tail at count slot x=144, ends 274, fits at width >= 442. Choose width 420:
// 372 > 420? no — count fits at 420. Need width in [442-eps? ] hmm: count fits iff
// width>=372, Tail-at-slot fits iff width>=442. For "count drops but tail-at-slot fits"
// we need width < 372 AND width >= 442 — impossible with countWidth==tailWidth region.
// So shrink countWidth to make its slot further right than tail's need. Use a spec where
// countWidth is large so it drops before a smaller Tail.
FooterBarSpec s2 = s;
s2.countWidth = 200; // large passive label
s2.tailWidth = 80; // small button
// toggle ends 138. count slot x = 144, count ends 344; fits iff 344 <= width-168 =>
// width >= 512. Tail at count slot x=144 ends 224; fits iff width >= 392.
// Choose width 420: count drops (needs 512), Tail-at-slot fits (needs 392).
FooterRect f{0, 0, 420, 26};
const FooterBarLayout L = computeFooterBar(f, s2);
CHECK(!L.toggle.empty());
CHECK(L.count.empty());
CHECK(!L.tail.empty());
// Tail sits at the count's slot (right after the toggle + gap), NOT past a phantom count.
CHECK(L.tail.x == L.toggle.x + L.toggle.width + s2.gap);
}
// A degenerate footer yields an all-empty layout (nothing placed, no crash).
static void testDegenerateFooterEmpty() {
const FooterBarSpec s = roundSpec();
const FooterBarLayout a = computeFooterBar(FooterRect{0, 0, 0, 26}, s);
CHECK(a.toggle.empty() && a.count.empty() && a.tail.empty());
const FooterBarLayout b = computeFooterBar(FooterRect{0, 0, 700, 0}, s);
CHECK(b.toggle.empty() && b.count.empty() && b.tail.empty());
// Height <= 2*verticalInset -> boxH <= 0 -> all empty.
const FooterBarLayout c = computeFooterBar(FooterRect{0, 0, 700, 8}, s);
CHECK(c.toggle.empty() && c.count.empty() && c.tail.empty());
}
// A footer too narrow even for the toggle: all empty (toggle drops last, so nothing survives).
static void testTooNarrowForToggle() {
const FooterBarSpec s = roundSpec();
// Toggle fits iff 8+130 <= width-168 => width >= 306. Width 200 suppresses everything.
const FooterBarLayout L = computeFooterBar(FooterRect{0, 0, 200, 26}, s);
CHECK(L.toggle.empty() && L.count.empty() && L.tail.empty());
}
// countWidth <= 0 hides the count label and the Tail sits right after the toggle (no count gap).
static void testCountHiddenByZeroWidth() {
FooterBarSpec s = roundSpec();
s.countWidth = 0;
FooterRect f{0, 0, 700, 26};
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(!L.toggle.empty());
CHECK(L.count.empty());
CHECK(!L.tail.empty());
CHECK(L.tail.x == L.toggle.x + L.toggle.width + s.gap); // right after the toggle
}
// --- Hit-test -----------------------------------------------------------------
static void testHitToggleAndTail() {
const FooterBarSpec s = roundSpec();
FooterRect f{0, 100, 700, 26};
const FooterBarLayout L = computeFooterBar(f, s);
// Toggle interior.
CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y, L) == FooterHit::Toggle);
CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width - 1,
L.toggle.y + L.toggle.height - 1, L) == FooterHit::Toggle);
// Tail interior.
CHECK(hitTestFooterBar(L.tail.x + L.tail.width / 2,
L.tail.y + L.tail.height / 2, L) == FooterHit::Tail);
// The count label is a passive readout — never a hit target.
CHECK(hitTestFooterBar(L.count.x + L.count.width / 2,
L.count.y + L.count.height / 2, L) == FooterHit::None);
// Between the toggle and the count (the gap) is a clean miss.
CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width, L.toggle.y, L) == FooterHit::None);
}
// Half-open bounds: far edges excluded; points outside every box miss.
static void testHitEdgesAndOutside() {
const FooterBarSpec s = roundSpec();
FooterRect f{0, 100, 700, 26};
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(hitTestFooterBar(L.toggle.x - 1, L.toggle.y, L) == FooterHit::None);
CHECK(hitTestFooterBar(L.tail.x + L.tail.width, L.tail.y, L) == FooterHit::None); // right edge excl
CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y - 1, L) == FooterHit::None); // above
CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y + L.toggle.height, L) == FooterHit::None); // below excl
// Far right (the prune/version region) is not this module's — a clean None here.
CHECK(hitTestFooterBar(f.x + f.width - 10, L.toggle.y, L) == FooterHit::None);
}
// A suppressed box claims no point — a Tail hit-test where the Tail was dropped is None.
static void testSuppressedClaimsNothing() {
const FooterBarSpec s = roundSpec();
FooterRect f{0, 0, 480, 26}; // Tail suppressed (see testTailSuppressedWhenTight)
const FooterBarLayout L = computeFooterBar(f, s);
CHECK(L.tail.empty());
// A point where the Tail WOULD have been claims nothing.
CHECK(hitTestFooterBar(210 + 10, 104, L) != FooterHit::Tail);
}
// --- Resize sweep: no overlap or cross-into-reserved across a width range -----
static void testResizeSweepNoOverlap() {
const FooterBarSpec s = roundSpec();
const int rightBoundFor = -s.rightReserve; // + width applied per iteration
for (int w = 40; w <= 1200; w += 7) {
FooterRect f{0, 0, w, 26};
const FooterBarLayout L = computeFooterBar(f, s);
const int rightBound = f.x + w + rightBoundFor; // = w - rightReserve
// Order + no overlap among placed boxes.
if (!L.toggle.empty() && !L.count.empty())
CHECK(L.count.x >= L.toggle.x + L.toggle.width);
if (!L.count.empty() && !L.tail.empty())
CHECK(L.tail.x >= L.count.x + L.count.width);
if (!L.toggle.empty() && L.count.empty() && !L.tail.empty())
CHECK(L.tail.x >= L.toggle.x + L.toggle.width);
// Nothing placed crosses into the reserved right region.
if (!L.toggle.empty()) CHECK(L.toggle.x + L.toggle.width <= rightBound);
if (!L.count.empty()) CHECK(L.count.x + L.count.width <= rightBound);
if (!L.tail.empty()) CHECK(L.tail.x + L.tail.width <= rightBound);
// A later affordance is never placed while an earlier one is dropped (greedy order):
// count/tail present implies toggle present.
if (!L.count.empty() || !L.tail.empty()) CHECK(!L.toggle.empty());
}
}
// --- Mode-segment enablement (the playback gate's visible half) ---------------
static void testModeSegmentsAreLiveWithTheTransportStopped() {
CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/true));
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/true));
}
static void testInactiveSegmentGoesDeadWhileTheTransportRuns() {
// The one that would fire a real switch — refused while playing/recording, so it
// must read dead rather than invite a click that silently does nothing.
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true, /*routable=*/true));
}
static void testActiveSegmentStaysLiveWhileTheTransportRuns() {
// Clicking the mode you are already in is a reapply, which is never gated;
// dimming it would read as "this mode is unavailable".
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true, /*routable=*/false));
}
static void testInactiveSegmentGoesDeadWhenUnroutable() {
// A third registered mode with no seeded command id (the model is N-mode, the UI
// ships two ids) must read dead, not live-but-silently-inert on click.
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/false));
}
static void testActiveSegmentStaysLiveEvenWhenUnroutable() {
// The active segment always fires a reapply through its own already-resolved id in
// practice, but the predicate's active bypass does not consult routable either way.
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/false));
}
int main() {
testWideFooterAllPlaced();
testOffsetFooterAnchorsLeft();
testTailSuppressedWhenTight();
testOnlyToggleSurvives();
testTailTakesCountSlotWhenCountDrops();
testDegenerateFooterEmpty();
testTooNarrowForToggle();
testCountHiddenByZeroWidth();
testHitToggleAndTail();
testHitEdgesAndOutside();
testSuppressedClaimsNothing();
testResizeSweepNoOverlap();
testModeSegmentsAreLiveWithTheTransportStopped();
testInactiveSegmentGoesDeadWhileTheTransportRuns();
testActiveSegmentStaysLiveWhileTheTransportRuns();
testInactiveSegmentGoesDeadWhenUnroutable();
testActiveSegmentStaysLiveEvenWhenUnroutable();
if (g_fail == 0) std::printf("footer_bar: all tests passed\n");
else std::printf("footer_bar: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}