test(cpp): tests reales para tween_curves, pie/kpi/bar math

- test_tween_curves: boundary conditions (t=0, t=0.5, t=1), monotonicidad
  para curvas no oscilantes, dispatch via apply(), names() no nulos.
- test_pie_chart_math: replica slice_at (anonymous namespace en pie_chart.cpp)
  y testea hit-test angular, edge del radio, distribucion proporcional.
- test_kpi_card_math: classify_delta (Up/Down/Flat) y pct_change con
  zero/negativos. La logica visual la cubre primitives_gallery (issue 0048).
- test_bar_chart_math: compute_y_range (incluye 0, negativos, vacio,
  single value) y clamp_bar_width [0.05, 1.0].
This commit is contained in:
2026-04-28 23:42:22 +02:00
parent 93a964a47d
commit 0779c34ca8
4 changed files with 356 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
// Tests para la logica matematica de pie_chart (slice hit-testing).
//
// La funcion `slice_at` real vive en namespace anonimo en pie_chart.cpp y
// esta tightly coupled con ImPlot. Aqui reproducimos el algoritmo y lo
// testeamos para garantizar que la matematica es correcta. Si la implementacion
// real se refactoriza para exponer la logica pura, este test se actualiza
// para usar el header publico (ver issue 0048 / refactor pendiente).
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include <cmath>
namespace {
// Mismo algoritmo que pie_chart.cpp::slice_at (verbatim).
template <typename T>
int slice_at(const T* values, int count, double total, double mouse_x,
double mouse_y, double cx, double cy, double radius) {
double dx = mouse_x - cx;
double dy = mouse_y - cy;
double r = std::sqrt(dx * dx + dy * dy);
if (r > radius) return -1;
constexpr double kPI = 3.14159265358979323846;
double angle_deg = std::atan2(dy, dx) * 180.0 / kPI;
double offset = angle_deg - 90.0;
while (offset < 0.0) offset += 360.0;
while (offset >= 360.0) offset -= 360.0;
double acc = 0.0;
for (int i = 0; i < count; i++) {
double sweep = (static_cast<double>(values[i]) / total) * 360.0;
if (offset >= acc && offset < acc + sweep) return i;
acc += sweep;
}
return count - 1;
}
} // namespace
TEST_CASE("pie_chart::slice_at returns -1 outside radius", "[pie_chart]") {
double values[] = {1.0, 1.0, 1.0, 1.0};
double total = 4.0;
// Mouse muy lejos del centro.
REQUIRE(slice_at(values, 4, total, 5.0, 5.0, 0.5, 0.5, 0.4) == -1);
}
TEST_CASE("pie_chart::slice_at on cursor at center returns first slice", "[pie_chart]") {
double values[] = {1.0, 1.0, 1.0, 1.0};
double total = 4.0;
// En el centro, dx=dy=0 -> atan2(0,0)=0 -> offset=-90 -> 270.
// Con 4 slices iguales (cada una 90 grados), 270 cae en la 4a slice (idx 3).
int idx = slice_at(values, 4, total, 0.5, 0.5, 0.5, 0.5, 0.4);
REQUIRE(idx >= 0);
REQUIRE(idx < 4);
}
TEST_CASE("pie_chart::slice_at directly above center returns first slice", "[pie_chart]") {
// 4 slices iguales. offset=0 (arriba) cae en idx 0.
double values[] = {1.0, 1.0, 1.0, 1.0};
int idx = slice_at(values, 4, 4.0, 0.5, 0.7, 0.5, 0.5, 0.4);
REQUIRE(idx == 0);
}
TEST_CASE("pie_chart::slice_at right of center hits second slice (CCW)", "[pie_chart]") {
// ImPlot dibuja CCW desde 90 grados (arriba). Yendo CCW: arriba -> izq -> abajo -> der.
// Mouse a la derecha: offset = atan2(0, +x)=0 deg -> -90 -> 270.
// 4 slices: [0,90)=0, [90,180)=1, [180,270)=2, [270,360)=3 -> idx 3.
double values[] = {1.0, 1.0, 1.0, 1.0};
int idx = slice_at(values, 4, 4.0, 0.7, 0.5, 0.5, 0.5, 0.4);
REQUIRE(idx == 3);
}
TEST_CASE("pie_chart::slice_at single slice always returns 0", "[pie_chart]") {
double values[] = {1.0};
REQUIRE(slice_at(values, 1, 1.0, 0.5, 0.7, 0.5, 0.5, 0.4) == 0);
REQUIRE(slice_at(values, 1, 1.0, 0.7, 0.5, 0.5, 0.5, 0.4) == 0);
REQUIRE(slice_at(values, 1, 1.0, 0.5, 0.3, 0.5, 0.5, 0.4) == 0);
}
TEST_CASE("pie_chart::slice_at right at radius edge", "[pie_chart]") {
double values[] = {1.0, 1.0};
// Justo en el borde del radio: r == radius -> r > radius es false, hits.
int idx = slice_at(values, 2, 2.0, 0.5 + 0.4, 0.5, 0.5, 0.5, 0.4);
REQUIRE(idx >= 0);
// Justo fuera del borde.
REQUIRE(slice_at(values, 2, 2.0, 0.5 + 0.41, 0.5, 0.5, 0.5, 0.4) == -1);
}
TEST_CASE("pie_chart::slice_at unequal slices distributes proportionally", "[pie_chart]") {
// Una slice grande (75%) y otra pequena (25%).
double values[] = {3.0, 1.0};
double total = 4.0;
// Slice 0: offset [0, 270). Slice 1: offset [270, 360).
// Mouse arriba (offset=0) -> idx 0.
REQUIRE(slice_at(values, 2, total, 0.5, 0.7, 0.5, 0.5, 0.4) == 0);
}