757e1585d6
Waveform overlay now resolves node/tab/marker click collisions by target area instead of check order; residue test now uses a distinguishing fixture; Gate-unavailable-while-drawn logic extracted to one pure helper shared by resolvePlay and applyControl.
361 lines
19 KiB
C++
361 lines
19 KiB
C++
// Standalone tests for reasampler::instrument::ui::knob_deck — no VST3, no REAPER, no framework. Same fast
|
|
// assert loop as the sibling pure tests. Assert the r11 deck layout HARD:
|
|
//
|
|
// * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths.
|
|
// * layout — caption toggle right-anchored IN the caption row; cells abutting left-to-right
|
|
// inside the box; knob square centered; label band beneath; row toggle after the cells.
|
|
// * reserves — a -1 id holds the group's width and hands its pixels to the cells present.
|
|
// * wrap — deterministic whole-group wrap at a narrowing width; the first group of a row
|
|
// always places; deckHeight consistency with deckRowCount.
|
|
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, fence padding
|
|
// misses, outside-deck misses.
|
|
|
|
#include "../src/core/instrument/ui/knob_deck.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
using namespace reasampler;
|
|
using namespace reasampler::instrument::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 representative deck shaped like the shell's: AMP (5 cells + caption toggle), PITCH
|
|
// (1 cell + caption toggle), PITCH ENV (3 cells + caption toggle), VOICE (1 cell + caption
|
|
// toggle + row toggle), MASTER (1 cell, no toggle).
|
|
static std::vector<DeckGroupDesc> shellLikeDeck() {
|
|
std::vector<DeckGroupDesc> g;
|
|
g.push_back({0, 78, {}, {100, 44}, {}, {1, 2, 3, 4, 5}, {}});
|
|
g.push_back({1, 38, {}, {101, 48}, {}, {6}, {}});
|
|
g.push_back({2, 58, {}, {102, 32}, {}, {7, 8, 9}, {}});
|
|
g.push_back({3, 38, {}, {103, 40}, {}, {10}, {104, 44}});
|
|
g.push_back({4, 46, {}, {}, {}, {11}, {}});
|
|
return g;
|
|
}
|
|
|
|
static void testGroupWidth() {
|
|
// Knob row dominates: 5 cells (240) > caption row (78 + 4 + 88 = 170) -> 240 + 2*6.
|
|
DeckGroupDesc amp{0, 78, {}, {100, 44}, {}, {1, 2, 3, 4, 5}, {}};
|
|
CHECK(deckGroupWidth(amp) == 5 * kDeckCellW + 2 * kDeckGroupPadX);
|
|
// Caption row dominates: 38 + 4 + 96 = 138 > 48 -> 138 + 12.
|
|
DeckGroupDesc pitch{1, 38, {}, {101, 48}, {}, {6}, {}};
|
|
CHECK(deckGroupWidth(pitch) == 38 + kDeckToggleGap + 2 * 48 + 2 * kDeckGroupPadX);
|
|
// Row toggle counts into the knob row: 48 + 4 + 88 = 140 > caption 38+4+80=122.
|
|
DeckGroupDesc voice{3, 38, {}, {103, 40}, {}, {10}, {104, 44}};
|
|
CHECK(deckGroupWidth(voice) ==
|
|
kDeckCellW + kDeckToggleGap + 2 * 44 + 2 * kDeckGroupPadX);
|
|
// No toggles: max(caption, cells) + padding.
|
|
DeckGroupDesc master{4, 46, {}, {}, {}, {11}, {}};
|
|
CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
|
|
}
|
|
|
|
static void testWrapAtNarrowWidthIsDeterministic() {
|
|
// A width that forces this synthetic deck to wrap: TWO rows, whole trailing groups only.
|
|
// Deliberately narrower than the shipped editor floor — this pins the wrap MECHANISM, not
|
|
// the shipped deck's row count (that is deck_groups' own test).
|
|
const auto deck = shellLikeDeck();
|
|
CHECK(deckRowCount(deck, 544) == 2);
|
|
CHECK(deckHeight(deck, 544) == 2 * kDeckGroupH + kDeckRowGap);
|
|
const DeckLayout dl = layoutDeck(deck, 8, 100, 544);
|
|
CHECK(dl.rowCount == 2);
|
|
CHECK(dl.height == deckHeight(deck, 544));
|
|
CHECK(dl.groups.size() == 5);
|
|
// Row membership: groups on row 1 share the first top; the wrapped groups sit one row
|
|
// pitch lower and restart at the left margin.
|
|
const int row0Top = dl.groups[0].box.y;
|
|
const int row1Top = row0Top + kDeckGroupH + kDeckRowGap;
|
|
CHECK(dl.groups[0].box.y == row0Top);
|
|
CHECK(dl.groups[1].box.y == row0Top);
|
|
bool sawWrap = false;
|
|
for (std::size_t i = 1; i < dl.groups.size(); ++i) {
|
|
if (dl.groups[i].box.y == row1Top && dl.groups[i - 1].box.y == row0Top) {
|
|
CHECK(dl.groups[i].box.x == 8); // wrapped row restarts at the left edge
|
|
sawWrap = true;
|
|
}
|
|
}
|
|
CHECK(sawWrap);
|
|
// Every box stays within the available width (no group straddles the right edge).
|
|
for (const auto& g : dl.groups) CHECK(g.box.right() <= 8 + 544);
|
|
}
|
|
|
|
static void testFirstGroupAlwaysPlaces() {
|
|
// A group wider than the row still places (degenerate width) — exactly one row per group.
|
|
const auto deck = shellLikeDeck();
|
|
CHECK(deckRowCount(deck, 100) == 5);
|
|
CHECK(deckHeight(deck, 100) == 5 * kDeckGroupH + 4 * kDeckRowGap);
|
|
}
|
|
|
|
static void testGroupInnerGeometry() {
|
|
const auto deck = shellLikeDeck();
|
|
const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
|
|
const DeckGroupLayout& amp = dl.groups[0];
|
|
// Caption row at the top padding; caption toggle right-anchored inside the box.
|
|
CHECK(amp.caption.y == amp.box.y + kDeckGroupPadY);
|
|
CHECK(amp.captionToggle.id == 100);
|
|
CHECK(amp.captionToggle.seg1.right() == amp.box.right() - kDeckGroupPadX);
|
|
CHECK(amp.captionToggle.seg0.right() == amp.captionToggle.seg1.x);
|
|
CHECK(amp.captionToggle.seg0.width == 44 && amp.captionToggle.seg1.width == 44);
|
|
CHECK(amp.captionToggle.seg0.height == kDeckToggleH);
|
|
// The caption text rect stops before the toggle.
|
|
CHECK(amp.caption.right() <= amp.captionToggle.seg0.x);
|
|
// Cells: five, fixed size, abutting, inside the box, below the caption row.
|
|
CHECK(static_cast<int>(amp.cells.size()) == 5);
|
|
for (std::size_t i = 0; i < amp.cells.size(); ++i) {
|
|
const DeckCellLayout& c = amp.cells[i];
|
|
CHECK(c.cell.width == kDeckCellW && c.cell.height == kDeckCellH);
|
|
CHECK(c.cell.y == amp.box.y + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap);
|
|
if (i > 0) CHECK(c.cell.x == amp.cells[i - 1].cell.right());
|
|
// Knob square centered horizontally, label band beneath it, both inside the cell.
|
|
CHECK(c.knob.width == kDeckKnobSize && c.knob.height == kDeckKnobSize);
|
|
CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
|
|
CHECK(c.label.y >= c.knob.bottom());
|
|
CHECK(c.label.bottom() <= c.cell.bottom());
|
|
}
|
|
// VOICE group's row toggle sits after its cell, vertically centered in the cell row.
|
|
const DeckGroupLayout& voice = dl.groups[3];
|
|
CHECK(voice.rowToggle.id == 104);
|
|
CHECK(voice.rowToggle.seg0.x == voice.cells[0].cell.right() + kDeckToggleGap);
|
|
CHECK(voice.rowToggle.seg0.height == kDeckToggleH);
|
|
CHECK(voice.rowToggle.seg0.y > voice.cells[0].cell.y);
|
|
// MASTER has no toggles.
|
|
CHECK(dl.groups[4].captionToggle.id == -1);
|
|
CHECK(dl.groups[4].rowToggle.id == -1);
|
|
}
|
|
|
|
static void testHitTest() {
|
|
const auto deck = shellLikeDeck();
|
|
const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
|
|
const DeckGroupLayout& amp = dl.groups[0];
|
|
|
|
// Knob hit: anywhere in the cell (including the label band) resolves to the cell id.
|
|
const DeckCellLayout& c0 = amp.cells[0];
|
|
DeckHit h = hitTestDeck(dl, c0.cell.x + 1, c0.cell.y + 1);
|
|
CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1);
|
|
h = hitTestDeck(dl, c0.label.x + 2, c0.label.y + 2);
|
|
CHECK(h.kind == DeckHitKind::Knob && h.id == 1);
|
|
|
|
// Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1.
|
|
h = hitTestDeck(dl, amp.captionToggle.seg0.right() - 1, amp.captionToggle.seg0.y + 1);
|
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0);
|
|
h = hitTestDeck(dl, amp.captionToggle.seg1.x, amp.captionToggle.seg1.y + 1);
|
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1);
|
|
|
|
// Row toggle.
|
|
const DeckGroupLayout& voice = dl.groups[3];
|
|
h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1);
|
|
CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);
|
|
|
|
// A reserve (id -1) yields no cell of its own. This fixture's reserve divides its present
|
|
// cells evenly (5 slots / 3 present -> 240/3, no residue), so every point of the knob row
|
|
// lands on a real control: no dead rect survives for a grab to fall into. That does NOT
|
|
// generalize to an indivisible reserve — a residue leaves a few uncovered margin pixels by
|
|
// design (testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds, below).
|
|
std::vector<DeckGroupDesc> trig;
|
|
trig.push_back({0, 78, {}, {100, 44}, {}, {20, 21, 22, -1, -1}, {}});
|
|
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
|
|
const DeckGroupLayout& tg = tl.groups[0];
|
|
CHECK(tg.cells.size() == 3);
|
|
for (const DeckCellLayout& c : tg.cells) CHECK(c.id >= 0);
|
|
// Bound the sweep against the RESERVED run (5 slots, not the 3 present cells) rather than
|
|
// the cells' own extent — the cells are what's under test, so deriving the bound from them
|
|
// could never catch a layout that under-covers the run they were reserved out of.
|
|
const int runStart = tg.box.x + kDeckGroupPadX;
|
|
const int runEnd = runStart + static_cast<int>(trig[0].cellIds.size()) * kDeckCellW;
|
|
const int rowY = tg.cells.back().cell.y + 5;
|
|
for (int px = runStart; px < runEnd; ++px) {
|
|
const DeckHit rowHit = hitTestDeck(tl, px, rowY);
|
|
CHECK(rowHit.kind == DeckHitKind::Knob && rowHit.id >= 0);
|
|
}
|
|
|
|
// The fence padding inside the box misses; outside the deck misses.
|
|
h = hitTestDeck(dl, amp.box.x + 1, amp.box.bottom() - 1);
|
|
CHECK(h.kind == DeckHitKind::None);
|
|
h = hitTestDeck(dl, -50, -50);
|
|
CHECK(h.kind == DeckHitKind::None);
|
|
}
|
|
|
|
// A reserve holds the group's WIDTH and hands its pixels to the cells that are present. The
|
|
// three properties together are what stops a narrower face reading as a hole: the group is
|
|
// exactly as wide as the full-face one, the cells are uniform and abutting, and what they do
|
|
// not cover is smaller than one pixel per cell.
|
|
static void testReservedCellWidthGoesToTheCellsPresent() {
|
|
const DeckGroupDesc full{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24}, {}};
|
|
// Three, four, and a lone cell against the same five-slot reserve — 240/3, 240/4, 240/1.
|
|
const std::vector<std::vector<int>> faces = {
|
|
{20, 21, 22, -1, -1}, {20, 21, 22, 23, -1}, {20, -1, -1, -1, -1}};
|
|
for (const std::vector<int>& ids : faces) {
|
|
DeckGroupDesc narrow = full;
|
|
narrow.cellIds = ids;
|
|
CHECK(deckGroupWidth(narrow) == deckGroupWidth(full));
|
|
|
|
std::vector<DeckGroupDesc> g{narrow};
|
|
const DeckLayout dl = layoutDeck(g, 0, 0, 824);
|
|
const DeckGroupLayout& lay = dl.groups[0];
|
|
const int present = static_cast<int>(lay.cells.size());
|
|
CHECK(present == 5 - static_cast<int>(std::count(ids.begin(), ids.end(), -1)));
|
|
|
|
const int run = 5 * kDeckCellW;
|
|
for (int i = 0; i < present; ++i) {
|
|
const DeckCellLayout& c = lay.cells[static_cast<std::size_t>(i)];
|
|
CHECK(c.cell.width == lay.cells[0].cell.width); // uniform
|
|
CHECK(c.knob.width == kDeckKnobSize); // the dial itself is fixed
|
|
CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
|
|
if (i > 0) CHECK(c.cell.x == lay.cells[static_cast<std::size_t>(i - 1)].cell.right());
|
|
}
|
|
// Uncovered run is the indivisible residue only, split evenly at the two ends.
|
|
const int covered = lay.cells.back().cell.right() - lay.cells[0].cell.x;
|
|
CHECK(run - covered < present);
|
|
const int leadPad = lay.cells[0].cell.x - (lay.box.x + kDeckGroupPadX);
|
|
CHECK(leadPad == (run - covered) / 2);
|
|
}
|
|
|
|
// A reserve does not move the row toggle: it anchors past the whole run, so the FILTER
|
|
// group's law switch cannot drift when a neighbouring face changes shape.
|
|
DeckGroupDesc withToggle{1, 40, {}, {}, {}, {20, 21, 22, 23, 24}, {104, 44}};
|
|
std::vector<DeckGroupDesc> a{withToggle};
|
|
withToggle.cellIds = {20, 21, -1, -1, -1};
|
|
std::vector<DeckGroupDesc> b{withToggle};
|
|
CHECK(layoutDeck(a, 0, 0, 824).groups[0].rowToggle.seg0 ==
|
|
layoutDeck(b, 0, 0, 824).groups[0].rowToggle.seg0);
|
|
}
|
|
|
|
// The three faces above (240/3, 240/4, 240/1) all divide their run evenly, so none of them
|
|
// actually exercises "residue in symmetric end margins". An 8-slot reserve with 5 present
|
|
// (384/5 = 76 r4) does: residue 4 is the smallest case that can tell a symmetric split (2/2)
|
|
// apart from a trailing-only one (0/4) — a residue of 1 (0/1 vs 1/0... i.e. 0/1) can't, since
|
|
// leadPad = residue/2 rounds to 0 either way, which is exactly why this seam's earlier test
|
|
// passed without pinning the rule it was named for.
|
|
static void testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds() {
|
|
const DeckGroupDesc g{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24, -1, -1, -1}, {}};
|
|
std::vector<DeckGroupDesc> gs{g};
|
|
const DeckLayout dl = layoutDeck(gs, 0, 0, 824);
|
|
const DeckGroupLayout& lay = dl.groups[0];
|
|
CHECK(lay.cells.size() == 5);
|
|
|
|
const int run = 8 * kDeckCellW;
|
|
const int present = 5;
|
|
const int cellW = run / present; // 76: the same integer division the layout uses
|
|
const int expectedResidue = run - cellW * present; // 4
|
|
CHECK(expectedResidue == 4);
|
|
|
|
const int covered = lay.cells.back().cell.right() - lay.cells.front().cell.x;
|
|
CHECK(run - covered == expectedResidue);
|
|
const int leadPad = lay.cells.front().cell.x - (lay.box.x + kDeckGroupPadX);
|
|
const int trailPad = (lay.box.right() - kDeckGroupPadX) - lay.cells.back().cell.right();
|
|
// Hard literals, not just the formula: this is the case that actually distinguishes
|
|
// symmetric (2/2) from trailing-only (0/4) — see the comment above.
|
|
CHECK(leadPad == 2);
|
|
CHECK(trailPad == 2);
|
|
CHECK(leadPad == expectedResidue / 2);
|
|
CHECK(trailPad == expectedResidue - leadPad); // both ends share it, not one absorbing it
|
|
}
|
|
|
|
// The corner radio widens the caption row, takes the far corner, and pushes the caption
|
|
// toggle left of itself — the three properties the overlay-select switch relies on.
|
|
static void testCaptionRadioGeometryAndHit() {
|
|
const DeckGroupDesc bare{7, 78, {}, {200, 44}, {}, {1, 2}, {}};
|
|
const DeckGroupDesc withRadio{7, 78, {201}, {200, 44}, {}, {1, 2}, {}};
|
|
// Caption row grows by exactly gap + radio; the knob row is unchanged, so a group whose
|
|
// caption row already dominated grows by that much.
|
|
CHECK(deckGroupWidth(withRadio) - deckGroupWidth(bare) ==
|
|
kDeckToggleGap + kDeckRadioSize);
|
|
|
|
std::vector<DeckGroupDesc> g{withRadio};
|
|
const DeckLayout dl = layoutDeck(g, 0, 0, 800);
|
|
const DeckGroupLayout& lay = dl.groups[0];
|
|
CHECK(lay.captionRadio.id == 201);
|
|
CHECK(lay.captionRadio.box.width == kDeckRadioSize);
|
|
// Far corner: flush with the group's inner right edge.
|
|
CHECK(lay.captionRadio.box.right() == lay.box.right() - kDeckGroupPadX);
|
|
// The toggle sits entirely left of the radio, and the caption text left of the toggle.
|
|
CHECK(lay.captionToggle.seg1.right() <= lay.captionRadio.box.x);
|
|
CHECK(lay.caption.right() <= lay.captionToggle.seg0.x);
|
|
|
|
const DeckHit h = hitTestDeck(dl, lay.captionRadio.box.x + 2, lay.captionRadio.box.y + 2);
|
|
CHECK(h.kind == DeckHitKind::CaptionRadio && h.id == 201);
|
|
}
|
|
|
|
// The inner dial is a concentric sub-region of the knob: a grab there still names the cell,
|
|
// with `inner` set, so a cell with no inner value simply ignores the flag.
|
|
static void testInnerDialHit() {
|
|
const std::vector<DeckGroupDesc> g = shellLikeDeck();
|
|
const DeckLayout dl = layoutDeck(g, 0, 0, 900);
|
|
const DeckCellLayout& c = dl.groups[0].cells[0];
|
|
CHECK(c.inner.width == kDeckInnerDialSize && c.inner.height == kDeckInnerDialSize);
|
|
// Concentric with the knob square.
|
|
CHECK(c.inner.x + c.inner.width / 2 == c.knob.x + c.knob.width / 2);
|
|
CHECK(c.inner.y + c.inner.height / 2 == c.knob.y + c.knob.height / 2);
|
|
|
|
DeckHit h = hitTestDeck(dl, c.inner.x + c.inner.width / 2, c.inner.y + c.inner.height / 2);
|
|
CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && h.inner);
|
|
// A grab on the outer ring is the same cell WITHOUT the inner flag.
|
|
h = hitTestDeck(dl, c.knob.x + 1, c.knob.y + 1);
|
|
CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && !h.inner);
|
|
}
|
|
|
|
// captionToggle2 sits immediately left of captionToggle when both are present (no overlap, and
|
|
// the caption text stops before the LEFTMOST one), and takes captionToggle's own slot when
|
|
// captionToggle is absent — the shipped FILTER ENV group's exact shape (deck_groups.cpp).
|
|
static void testCaptionToggle2() {
|
|
const DeckGroupDesc both{9, 40, {}, {300, 30}, {301, 20}, {1, 2, 3}, {}};
|
|
std::vector<DeckGroupDesc> g{both};
|
|
const DeckLayout dl = layoutDeck(g, 0, 0, 800);
|
|
const DeckGroupLayout& lay = dl.groups[0];
|
|
CHECK(lay.captionToggle.id == 300);
|
|
CHECK(lay.captionToggle2.id == 301);
|
|
CHECK(lay.captionToggle2.seg0.width == 20 && lay.captionToggle2.seg1.width == 20);
|
|
// Left of the first, with exactly one gap between — no overlap by construction.
|
|
CHECK(lay.captionToggle2.seg1.right() == lay.captionToggle.seg0.x - kDeckToggleGap);
|
|
// Caption text stops before the LEFTMOST toggle (toggle2), not just the first-placed one.
|
|
CHECK(lay.caption.right() <= lay.captionToggle2.seg0.x);
|
|
|
|
DeckHit h = hitTestDeck(dl, lay.captionToggle2.seg0.right() - 1,
|
|
lay.captionToggle2.seg0.y + 1);
|
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 301 && h.segment == 0);
|
|
h = hitTestDeck(dl, lay.captionToggle2.seg1.x, lay.captionToggle2.seg1.y + 1);
|
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 301 && h.segment == 1);
|
|
|
|
// FILTER ENV's real shape: captionToggle absent, captionToggle2 present with a radio — it
|
|
// takes the first (rightmost) slot rather than leaving a gap where captionToggle would sit.
|
|
const DeckGroupDesc filterEnvLike{10, 66, {200}, {}, {302, 23}, {1, 2, 3, 4, 5}, {}};
|
|
std::vector<DeckGroupDesc> g2{filterEnvLike};
|
|
const DeckLayout dl2 = layoutDeck(g2, 0, 0, 800);
|
|
const DeckGroupLayout& fe = dl2.groups[0];
|
|
CHECK(fe.captionToggle.id == -1);
|
|
CHECK(fe.captionToggle2.id == 302);
|
|
CHECK(fe.captionToggle2.seg1.right() == fe.captionRadio.box.x - kDeckToggleGap);
|
|
h = hitTestDeck(dl2, fe.captionToggle2.seg1.x, fe.captionToggle2.seg1.y + 1);
|
|
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 302 && h.segment == 1);
|
|
}
|
|
|
|
static void testEmptyDeck() {
|
|
const std::vector<DeckGroupDesc> none;
|
|
CHECK(deckRowCount(none, 800) == 0);
|
|
CHECK(deckHeight(none, 800) == 0);
|
|
const DeckLayout dl = layoutDeck(none, 0, 0, 800);
|
|
CHECK(dl.groups.empty() && dl.rowCount == 0 && dl.height == 0);
|
|
}
|
|
|
|
int main() {
|
|
testGroupWidth();
|
|
testWrapAtNarrowWidthIsDeterministic();
|
|
testFirstGroupAlwaysPlaces();
|
|
testGroupInnerGeometry();
|
|
testHitTest();
|
|
testReservedCellWidthGoesToTheCellsPresent();
|
|
testIndivisibleResidueSplitsSymmetricallyAcrossBothEnds();
|
|
testCaptionRadioGeometryAndHit();
|
|
testInnerDialHit();
|
|
testCaptionToggle2();
|
|
testEmptyDeck();
|
|
if (g_fail) {
|
|
std::printf("%d FAILURE(S)\n", g_fail);
|
|
return 1;
|
|
}
|
|
std::printf("knob_deck tests passed\n");
|
|
return 0;
|
|
}
|