445 lines
20 KiB
C++
445 lines
20 KiB
C++
// Standalone tests for reasampler::ui::drag_out — no REAPER, no test framework. Same fast loop
|
|
// as the sibling pure tests: assert the gesture law and the path-list assembly directly.
|
|
//
|
|
// Covers:
|
|
// * The full class matrix: every ReaperSurface x {single, multi} x {track, no track}, inside
|
|
// and outside the client rect, plus the half-open edge and a non-zero panel origin.
|
|
// * Reversibility and speed-independence, stated as properties: a class transition sequence
|
|
// resolves the same forwards and backwards, and one unresolvable evaluation cannot change
|
|
// any later evaluation's outcome.
|
|
// * Exhaustiveness: no surface resolves to a do-nothing class, and every class has a cue.
|
|
// * Path-list assembly + OS hand-off ordering (unchanged from before this law).
|
|
|
|
#include "../src/core/ui/drag_out.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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)
|
|
|
|
// --- Fixtures -----------------------------------------------------------------
|
|
|
|
static const PanelClientRect kPanel{0, 0, 400, 300};
|
|
|
|
// A live single-card drag over `surface`, with a track resolved unless stated otherwise.
|
|
static DropContext ctx(ReaperSurface surface, bool single = true, bool haveTrack = true) {
|
|
DropContext c;
|
|
c.drag = DragState{/*dragging=*/true, /*hasArmedSamples=*/true};
|
|
c.singlePayload = single;
|
|
c.surface = surface;
|
|
c.haveTrack = haveTrack;
|
|
return c;
|
|
}
|
|
|
|
// Every surface the law enumerates, so the matrix tests iterate rather than list.
|
|
static const ReaperSurface kAllSurfaces[] = {
|
|
ReaperSurface::OffReaper, ReaperSurface::TrackPanel, ReaperSurface::FxSurface,
|
|
ReaperSurface::FxEmbed, ReaperSurface::Arrange, ReaperSurface::Other,
|
|
};
|
|
|
|
// Pins kAllSurfaces against ReaperSurface::Count so a 7th surface added to the enum without a
|
|
// matching entry here fails the BUILD, not just a silently-incomplete matrix — the compiler
|
|
// alone does not enforce this (no -Wswitch/-Wall or /W4 anywhere in the build; see
|
|
// panel_drag.cpp's onLBtnUp for the same caveat on DropClass).
|
|
static_assert(sizeof(kAllSurfaces) / sizeof(kAllSurfaces[0]) ==
|
|
static_cast<std::size_t>(ReaperSurface::Count),
|
|
"kAllSurfaces must list exactly the surfaces below ReaperSurface::Count");
|
|
|
|
// A point comfortably outside the panel client rect.
|
|
static const int kOutX = 500, kOutY = 150;
|
|
|
|
// --- Matrix: inside the client -------------------------------------------------
|
|
|
|
// Inside the client the drag is the bank-to-bank drag, whatever REAPER reports underneath and
|
|
// whatever the payload size — the internal path must never be reinterpreted as an FX add or a
|
|
// timeline insert. This is the bank-to-bank regression floor.
|
|
static void testInsideClientIsAlwaysInternal() {
|
|
for (ReaperSurface s : kAllSurfaces) {
|
|
for (bool single : {true, false}) {
|
|
CHECK(decideDropClass(200, 150, kPanel, ctx(s, single)) == DropClass::Internal);
|
|
CHECK(decideDropClass(0, 0, kPanel, ctx(s, single)) == DropClass::Internal);
|
|
CHECK(decideDropClass(399, 299, kPanel, ctx(s, single)) == DropClass::Internal);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The half-open boundary: x+width and y+height are OUTSIDE, the pixel just inside is Internal —
|
|
// matches the panel's other hit-tests so the edge is claimed consistently.
|
|
static void testBoundaryHalfOpen() {
|
|
const DropContext off = ctx(ReaperSurface::OffReaper);
|
|
CHECK(decideDropClass(399, 150, kPanel, off) == DropClass::Internal);
|
|
CHECK(decideDropClass(400, 150, kPanel, off) == DropClass::OsHandoff);
|
|
CHECK(decideDropClass(200, 299, kPanel, off) == DropClass::Internal);
|
|
CHECK(decideDropClass(200, 300, kPanel, off) == DropClass::OsHandoff);
|
|
}
|
|
|
|
// A non-zero panel origin — the boundary tracks the rect, not the absolute axes.
|
|
static void testOffsetPanelRect() {
|
|
const PanelClientRect p{50, 20, 100, 80}; // spans x[50,150) y[20,100)
|
|
const DropContext off = ctx(ReaperSurface::OffReaper);
|
|
CHECK(decideDropClass(100, 60, p, off) == DropClass::Internal);
|
|
CHECK(decideDropClass(49, 60, p, off) == DropClass::OsHandoff);
|
|
CHECK(decideDropClass(150, 60, p, off) == DropClass::OsHandoff);
|
|
CHECK(decideDropClass(100, 19, p, off) == DropClass::OsHandoff);
|
|
}
|
|
|
|
// --- Matrix: outside the client, single card -----------------------------------
|
|
|
|
// A single card over a track's panel loads the instrument — the WHOLE panel, which is the
|
|
// root-cause fix: a TCP too narrow to draw the FX button used to yield a cue-less no-op.
|
|
static void testSingleOverTrackPanelIsInstrumentDrop() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::TrackPanel)) ==
|
|
DropClass::InstrumentDrop);
|
|
CHECK(decideDropClass(-5, kOutY, kPanel, ctx(ReaperSurface::TrackPanel)) ==
|
|
DropClass::InstrumentDrop);
|
|
CHECK(decideDropClass(200, 400, kPanel, ctx(ReaperSurface::TrackPanel)) ==
|
|
DropClass::InstrumentDrop);
|
|
}
|
|
|
|
// The FX chain / floating-FX windows keep their existing outcome.
|
|
static void testSingleOverFxSurfaceIsInstrumentDrop() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
|
DropClass::InstrumentDrop);
|
|
}
|
|
|
|
// The embed strip is where an instance ALREADY draws: dropping there must not stack a second,
|
|
// so it refuses rather than instantiating.
|
|
static void testSingleOverFxEmbedRefuses() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxEmbed)) ==
|
|
DropClass::Refuse);
|
|
}
|
|
|
|
// THE HEADLINE DEFECT: a single card over the arrange places a timeline item. It used to lock
|
|
// to InstrumentDrop on the first move outside the client and then release into nothing.
|
|
static void testSingleOverArrangeIsArrangeInsert() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::Arrange)) ==
|
|
DropClass::ArrangeInsert);
|
|
}
|
|
|
|
// Ruler / transport / docker chrome / any token REAPER adds later: a defined refusal.
|
|
static void testSingleOverOtherReaperUiRefuses() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::Other)) == DropClass::Refuse);
|
|
}
|
|
|
|
// Off REAPER entirely -> the OS drag-out, the one irreversible transition.
|
|
static void testSingleOffReaperIsOsHandoff() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::OffReaper)) ==
|
|
DropClass::OsHandoff);
|
|
}
|
|
|
|
// --- Matrix: outside the client, multi card ------------------------------------
|
|
|
|
// A multi payload names no single instrument, so both instrument surfaces refuse — a DEFINED
|
|
// outcome with a cue, where the old law silently took the OS path from these surfaces.
|
|
static void testMultiOverInstrumentSurfacesRefuses() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::TrackPanel, false)) ==
|
|
DropClass::Refuse);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface, false)) ==
|
|
DropClass::Refuse);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxEmbed, false)) ==
|
|
DropClass::Refuse);
|
|
}
|
|
|
|
// Multi over the arrange still places — one item per capture; the shell lays them out.
|
|
static void testMultiOverArrangeIsArrangeInsert() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::Arrange, false)) ==
|
|
DropClass::ArrangeInsert);
|
|
}
|
|
|
|
// Multi off REAPER is the classic multi-file drag-out, unchanged.
|
|
static void testMultiOffReaperIsOsHandoff() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::OffReaper, false)) ==
|
|
DropClass::OsHandoff);
|
|
}
|
|
|
|
static void testMultiOverOtherReaperUiRefuses() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::Other, false)) ==
|
|
DropClass::Refuse);
|
|
}
|
|
|
|
// --- The documented null-track / non-empty-info case ---------------------------
|
|
|
|
// GetThingFromPoint "may return NULL with valid info string to indicate non-track thing". Both
|
|
// outcomes that need a track therefore refuse instead of dereferencing nothing: over the arrange
|
|
// that is the empty region below the last track, and over a track panel it is a surface we
|
|
// cannot attribute.
|
|
static void testNullTrackWithSurfaceRefuses() {
|
|
for (ReaperSurface s : {ReaperSurface::TrackPanel, ReaperSurface::FxSurface,
|
|
ReaperSurface::Arrange}) {
|
|
for (bool single : {true, false}) {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel,
|
|
ctx(s, single, /*haveTrack=*/false)) == DropClass::Refuse);
|
|
}
|
|
}
|
|
}
|
|
|
|
// A track under the pointer never turns OffReaper into a REAPER-internal outcome: OffReaper is
|
|
// the shell's "the info string was empty and there was no track" verdict, and the law trusts it.
|
|
static void testOffReaperIgnoresTrackFlag() {
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel,
|
|
ctx(ReaperSurface::OffReaper, true, false)) == DropClass::OsHandoff);
|
|
}
|
|
|
|
// --- Not-a-drag ----------------------------------------------------------------
|
|
|
|
// No armed samples, or not dragging -> None regardless of position or surface.
|
|
static void testNoDragOrNoSamplesIsNone() {
|
|
for (ReaperSurface s : kAllSurfaces) {
|
|
DropContext c = ctx(s);
|
|
c.drag = DragState{/*dragging=*/true, /*hasArmedSamples=*/false};
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::None);
|
|
CHECK(decideDropClass(200, 150, kPanel, c) == DropClass::None);
|
|
|
|
c.drag = DragState{/*dragging=*/false, /*hasArmedSamples=*/true};
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::None);
|
|
CHECK(decideDropClass(200, 150, kPanel, c) == DropClass::None);
|
|
}
|
|
}
|
|
|
|
// --- Reversibility, speed-independence, exhaustiveness -------------------------
|
|
|
|
// The transition sequence from the acceptance criteria: client -> arrange -> FX window ->
|
|
// arrange -> client. Every step resolves on its own terms, and the return leg reproduces the
|
|
// outgoing leg exactly — the instrument drop is still available after crossing the arrange, and
|
|
// re-entering the client resumes the internal drag.
|
|
static void testClassTransitionsAreReversible() {
|
|
const DropContext arrange = ctx(ReaperSurface::Arrange);
|
|
const DropContext fx = ctx(ReaperSurface::FxSurface);
|
|
const DropContext inside = ctx(ReaperSurface::Other); // surface is ignored inside
|
|
|
|
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, fx) == DropClass::InstrumentDrop);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
|
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
|
}
|
|
|
|
// One unresolvable evaluation (a surface with no track, which refuses) followed by a resolvable
|
|
// one: the second resolves exactly as it would have on its own. This is the property the retired
|
|
// drag-lifetime "blocked" latch violated.
|
|
static void testUnresolvableEvaluationDoesNotAffectTheNext() {
|
|
const DropContext trackless = ctx(ReaperSurface::Arrange, true, /*haveTrack=*/false);
|
|
const DropContext resolvable = ctx(ReaperSurface::Arrange);
|
|
const DropClass standalone = decideDropClass(kOutX, kOutY, kPanel, resolvable);
|
|
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, trackless) == DropClass::Refuse);
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, resolvable) == standalone);
|
|
CHECK(standalone == DropClass::ArrangeInsert);
|
|
|
|
// And in the other order — the refusal is equally uninfluenced by what preceded it.
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, trackless) == DropClass::Refuse);
|
|
}
|
|
|
|
// Drag speed only changes WHICH intermediate points get evaluated. Since the class is a function
|
|
// of the current point and context alone, a "fast flick" (one evaluation at the release point)
|
|
// and a "slow drag" (many evaluations ending at the same point) agree at that point — with the
|
|
// intermediate surfaces deliberately chosen to disagree with the destination.
|
|
static void testDragSpeedCannotChangeTheOutcome() {
|
|
const DropContext destination = ctx(ReaperSurface::TrackPanel);
|
|
const DropClass flick = decideDropClass(kOutX, kOutY, kPanel, destination);
|
|
|
|
// The slow path crosses everything else first.
|
|
for (ReaperSurface s : kAllSurfaces) {
|
|
(void)decideDropClass(kOutX - 10, kOutY, kPanel, ctx(s));
|
|
(void)decideDropClass(200, 150, kPanel, ctx(s)); // and back through the client
|
|
}
|
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, destination) == flick);
|
|
CHECK(flick == DropClass::InstrumentDrop);
|
|
}
|
|
|
|
// EXHAUSTIVENESS (acceptance criterion 7, structural rather than spot-checked): for a live drag
|
|
// outside the client, no surface x payload x track combination resolves to None — i.e. there is
|
|
// no cell whose release does nothing without having said so. None is reachable only from a dead
|
|
// drag, which is asserted separately above.
|
|
static void testNoLiveOutsideCombinationResolvesToNone() {
|
|
for (ReaperSurface s : kAllSurfaces) {
|
|
for (bool single : {true, false}) {
|
|
for (bool haveTrack : {true, false}) {
|
|
const DropClass c = decideDropClass(kOutX, kOutY, kPanel, ctx(s, single, haveTrack));
|
|
CHECK(c != DropClass::None);
|
|
CHECK(c != DropClass::Internal);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every class carries a cue, and only the two the shell deliberately does not draw map to a
|
|
// no-cursor cue — so "will this work" is answerable before release for every resolvable class.
|
|
static void testEveryClassHasACue() {
|
|
CHECK(cueForDropClass(DropClass::Internal) == DropCue::Internal);
|
|
CHECK(cueForDropClass(DropClass::InstrumentDrop) == DropCue::Instrument);
|
|
CHECK(cueForDropClass(DropClass::ArrangeInsert) == DropCue::ArrangeInsert);
|
|
CHECK(cueForDropClass(DropClass::Refuse) == DropCue::Refuse);
|
|
CHECK(cueForDropClass(DropClass::OsHandoff) == DropCue::OsOwned);
|
|
CHECK(cueForDropClass(DropClass::None) == DropCue::None);
|
|
}
|
|
|
|
// --- Path-list assembly -------------------------------------------------------
|
|
|
|
static ResolvedSample ok(const std::string& p) { return ResolvedSample{p, true}; }
|
|
static ResolvedSample missing(const std::string& p) { return ResolvedSample{p, false}; }
|
|
static ResolvedSample unresolved() { return ResolvedSample{"", false}; }
|
|
|
|
static void testSinglePath() {
|
|
PathList l = assemblePathList({ok("C:/proj/bank/a.wav")});
|
|
CHECK(l.paths.size() == 1);
|
|
CHECK(l.paths[0] == "C:/proj/bank/a.wav");
|
|
CHECK(l.skippedMissing == 0);
|
|
CHECK(l.skippedUnresolved == 0);
|
|
CHECK(l.skippedDuplicate == 0);
|
|
}
|
|
|
|
// Multiple distinct paths pass through in selection order (order preserved).
|
|
static void testMultiPreservesOrder() {
|
|
PathList l = assemblePathList({ok("b.wav"), ok("a.wav"), ok("c.wav")});
|
|
CHECK(l.paths.size() == 3);
|
|
CHECK(l.paths[0] == "b.wav");
|
|
CHECK(l.paths[1] == "a.wav");
|
|
CHECK(l.paths[2] == "c.wav");
|
|
}
|
|
|
|
// Two index entries resolving to the SAME file (the cross-bank copy case — one file, two
|
|
// entries) yield ONE path; the extra is counted, first occurrence wins.
|
|
static void testDedupeSamePath() {
|
|
PathList l = assemblePathList({ok("x.wav"), ok("y.wav"), ok("x.wav")});
|
|
CHECK(l.paths.size() == 2);
|
|
CHECK(l.paths[0] == "x.wav");
|
|
CHECK(l.paths[1] == "y.wav");
|
|
CHECK(l.skippedDuplicate == 1);
|
|
}
|
|
|
|
// A stale index entry (file gone from disk) is skipped — never a dangling path handed onward.
|
|
static void testSkipMissing() {
|
|
PathList l = assemblePathList({ok("a.wav"), missing("gone.wav"), ok("b.wav")});
|
|
CHECK(l.paths.size() == 2);
|
|
CHECK(l.paths[0] == "a.wav");
|
|
CHECK(l.paths[1] == "b.wav");
|
|
CHECK(l.skippedMissing == 1);
|
|
}
|
|
|
|
// An unresolvable entry (empty path — no project dir / empty relative) is skipped; note an
|
|
// empty path is skippedUnresolved, NOT skippedMissing, even though fileExists is false.
|
|
static void testSkipUnresolved() {
|
|
PathList l = assemblePathList({ok("a.wav"), unresolved(), ok("b.wav")});
|
|
CHECK(l.paths.size() == 2);
|
|
CHECK(l.skippedUnresolved == 1);
|
|
CHECK(l.skippedMissing == 0);
|
|
}
|
|
|
|
// Empty selection -> empty list, all tallies zero (the shell reads paths.empty() and does
|
|
// not start a drag).
|
|
static void testEmptySelection() {
|
|
PathList l = assemblePathList({});
|
|
CHECK(l.paths.empty());
|
|
CHECK(l.skippedMissing == 0);
|
|
CHECK(l.skippedUnresolved == 0);
|
|
CHECK(l.skippedDuplicate == 0);
|
|
}
|
|
|
|
// All-skipped selection -> empty list with the right tallies (nothing draggable).
|
|
static void testAllSkippedYieldsEmpty() {
|
|
PathList l = assemblePathList({missing("g1.wav"), unresolved(), missing("g2.wav")});
|
|
CHECK(l.paths.empty());
|
|
CHECK(l.skippedMissing == 2);
|
|
CHECK(l.skippedUnresolved == 1);
|
|
}
|
|
|
|
// Mixed: every skip class at once, plus a survivor, with independent tallies.
|
|
static void testMixedTallies() {
|
|
PathList l = assemblePathList({
|
|
ok("keep.wav"), // survives
|
|
missing("gone.wav"), // skippedMissing
|
|
unresolved(), // skippedUnresolved
|
|
ok("keep.wav"), // skippedDuplicate
|
|
ok("also.wav"), // survives
|
|
});
|
|
CHECK(l.paths.size() == 2);
|
|
CHECK(l.paths[0] == "keep.wav");
|
|
CHECK(l.paths[1] == "also.wav");
|
|
CHECK(l.skippedMissing == 1);
|
|
CHECK(l.skippedUnresolved == 1);
|
|
CHECK(l.skippedDuplicate == 1);
|
|
}
|
|
|
|
// --- OS hand-off ordering -----------------------------------------------------
|
|
//
|
|
// The empty-payload teardown regression: the shell used to wind its internal drag down
|
|
// (release capture, clear drag state) BEFORE checking whether the payload had resolved to any
|
|
// on-disk file. For an unresolvable payload, initiateDragOut is never reached — no OS drop
|
|
// happens at all — but the teardown ran anyway, so the drag just silently stopped mid-gesture
|
|
// with no highlight and no drop. These pin the fix: the two side effects (teardown, hand-off)
|
|
// are one decision.
|
|
|
|
// Nothing draggable -> hand off nothing AND keep the internal drag alive.
|
|
static void testEmptyPathsHandsOffNothingAndKeepsInternalDrag() {
|
|
const OsHandoff h = decideOsHandoff({});
|
|
CHECK(!h.handOffToOs);
|
|
}
|
|
|
|
// A resolvable payload -> hand off (release the internal drag, then start the OS drag).
|
|
static void testResolvablePayloadHandsOff() {
|
|
const OsHandoff one = decideOsHandoff({"C:/proj/bank/a.wav"});
|
|
CHECK(one.handOffToOs);
|
|
const OsHandoff many = decideOsHandoff({"a.wav", "b.wav", "c.wav"});
|
|
CHECK(many.handOffToOs);
|
|
}
|
|
|
|
// End to end through the assembler: a selection whose every entry is stale/unresolvable
|
|
// yields the keep-the-drag verdict, which is exactly the case the shell used to mishandle.
|
|
static void testAllSkippedSelectionKeepsInternalDrag() {
|
|
const PathList l = assemblePathList({missing("g1.wav"), unresolved()});
|
|
const OsHandoff h = decideOsHandoff(l.paths);
|
|
CHECK(!h.handOffToOs);
|
|
}
|
|
|
|
int main() {
|
|
testInsideClientIsAlwaysInternal();
|
|
testBoundaryHalfOpen();
|
|
testOffsetPanelRect();
|
|
|
|
testSingleOverTrackPanelIsInstrumentDrop();
|
|
testSingleOverFxSurfaceIsInstrumentDrop();
|
|
testSingleOverFxEmbedRefuses();
|
|
testSingleOverArrangeIsArrangeInsert();
|
|
testSingleOverOtherReaperUiRefuses();
|
|
testSingleOffReaperIsOsHandoff();
|
|
|
|
testMultiOverInstrumentSurfacesRefuses();
|
|
testMultiOverArrangeIsArrangeInsert();
|
|
testMultiOffReaperIsOsHandoff();
|
|
testMultiOverOtherReaperUiRefuses();
|
|
|
|
testNullTrackWithSurfaceRefuses();
|
|
testOffReaperIgnoresTrackFlag();
|
|
testNoDragOrNoSamplesIsNone();
|
|
|
|
testClassTransitionsAreReversible();
|
|
testUnresolvableEvaluationDoesNotAffectTheNext();
|
|
testDragSpeedCannotChangeTheOutcome();
|
|
testNoLiveOutsideCombinationResolvesToNone();
|
|
testEveryClassHasACue();
|
|
|
|
testSinglePath();
|
|
testMultiPreservesOrder();
|
|
testDedupeSamePath();
|
|
testSkipMissing();
|
|
testSkipUnresolved();
|
|
testEmptySelection();
|
|
testAllSkippedYieldsEmpty();
|
|
testMixedTallies();
|
|
|
|
testEmptyPathsHandsOffNothingAndKeepsInternalDrag();
|
|
testResolvablePayloadHandsOff();
|
|
testAllSkippedSelectionKeepsInternalDrag();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|