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