blob: 3dd6ee19fe2d1b8ea5db9e3457dbe0e39686f7ef [file]
/*
* Copyright 2026 Rive
*/
// Tests for SemanticProvider — the static utility that resolves a
// Component's semantic data and bounds. Exercises the three strategies:
// 1. Explicit SemanticData child on the component.
// 2. Inference (currently Text → role=text, label from runs).
// 3. Bounds: drawable world-bounds → rootTransform → root artboard space.
//
// The bounds-in-root-space property is verified by cross-checking the
// diff output against the raw SemanticProvider::semanticBounds call on
// the source Component.
#include <rive/animation/state_machine_instance.hpp>
#include <rive/artboard.hpp>
#include <rive/node.hpp>
#include <rive/semantic/semantic_data.hpp>
#include <rive/semantic/semantic_provider.hpp>
#include <rive/semantic/semantic_role.hpp>
#include <rive/text/text.hpp>
#include "rive_file_reader.hpp"
#include "semantic_test_helpers.hpp"
#include <catch.hpp>
using namespace rive;
using rive::semantic_test::advance;
// ---------------------------------------------------------------------------
// Null-input safety.
// ---------------------------------------------------------------------------
TEST_CASE("SemanticProvider::canInferSemantics(nullptr) returns false",
"[semantics][provider]")
{
CHECK_FALSE(SemanticProvider::canInferSemantics(nullptr));
}
TEST_CASE("SemanticProvider::resolveSemanticData(nullptr) returns default",
"[semantics][provider]")
{
auto out = SemanticProvider::resolveSemanticData(nullptr);
CHECK_FALSE(out.hasSemantics);
CHECK(out.role == 0);
CHECK(out.label.empty());
}
TEST_CASE("SemanticProvider::semanticBounds(nullptr) returns empty",
"[semantics][provider]")
{
const auto bounds = SemanticProvider::semanticBounds(nullptr);
CHECK(bounds.isEmptyOrNaN());
}
// ---------------------------------------------------------------------------
// Inference: Text component resolves to role=text with concatenated runs.
// ---------------------------------------------------------------------------
TEST_CASE("canInferSemantics(Text) returns true for any Text component",
"[semantics][provider]")
{
auto file = ReadRiveFile("assets/semantic/simpsons.riv");
auto artboard = file->artboardDefault();
// Advance so text lays out and runs are populated.
auto sm = artboard->stateMachineAt(0);
advance(sm.get());
bool sawText = false;
for (auto* text : artboard->objects<Text>())
{
REQUIRE(text != nullptr);
CHECK(SemanticProvider::canInferSemantics(text));
sawText = true;
}
// Simpsons fixture has labelled tabs — must contain at least one Text.
CHECK(sawText);
}
TEST_CASE("resolveSemanticData on a Node hosting a SemanticData child uses the "
"explicit role/label",
"[semantics][provider]")
{
// Use simpsons.riv, which has SDs on tab-role Nodes. Walk every SD's
// parent Node; resolving that Node must produce the SD's authored
// role and label (no inference fallback).
auto file = ReadRiveFile("assets/semantic/simpsons.riv");
auto artboard = file->artboardDefault();
auto sm = artboard->stateMachineAt(0);
advance(sm.get());
int checked = 0;
for (auto* sd : artboard->objects<SemanticData>())
{
auto* host = sd->parent();
if (host == nullptr)
{
continue;
}
auto resolved = SemanticProvider::resolveSemanticData(host);
CHECK(resolved.hasSemantics);
CHECK(resolved.role == sd->role());
CHECK(resolved.label == sd->label());
checked++;
}
CHECK(checked > 0);
}
// ---------------------------------------------------------------------------
// semanticBounds — non-empty, in root artboard space.
// ---------------------------------------------------------------------------
TEST_CASE(
"semanticBounds on a laid-out drawable Node produces non-empty bounds",
"[semantics][provider]")
{
auto file = ReadRiveFile("assets/semantic/simpsons.riv");
auto artboard = file->artboardDefault();
auto sm = artboard->stateMachineAt(0);
advance(sm.get());
// Any Node that hosts a SemanticData child in this artboard is drawable-
// backed (it's a real button/tab). Verify semanticBounds is non-empty.
int checked = 0;
for (auto* sd : artboard->objects<SemanticData>())
{
auto* host = sd->parent();
if (host == nullptr)
{
continue;
}
const auto bounds = SemanticProvider::semanticBounds(host);
CAPTURE(sd->label());
CHECK_FALSE(bounds.isEmptyOrNaN());
CHECK(bounds.width() > 0.0f);
CHECK(bounds.height() > 0.0f);
checked++;
}
CHECK(checked > 0);
}