| /* | |
| * 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); | |
| } |