logo SERENITY
Game Systems Foundation
Search Results for

    Show / Hide Table of Contents

    Class UnityHudElementViewTests

    PlayMode tests for UnityHudElementView built the way the game builds it — through Build, with a live service resolver — rather than through the preview.

    Inheritance
    object
    UnityHudElementViewTests
    Inherited Members
    object.ToString()
    object.Equals(object)
    object.Equals(object, object)
    object.ReferenceEquals(object, object)
    object.GetHashCode()
    object.GetType()
    object.MemberwiseClone()
    Namespace: Serenity.UnityGameUi.Tests.Infrastructure
    Assembly: Tests.dll
    Syntax
    public class UnityHudElementViewTests
    Remarks

    Nothing in the suite reached this path before. The preview and the game share Construct, so placement and styling were exercised indirectly, but binding against a real service, the update triggers and the runtime resource bundle were not covered at all — and those are the parts that only exist when the game is running.

    Elements are authored by reflection over the serialized fields on purpose: the fields are private because authoring is the inspector's job, and a test that needed them public would be asking production code to widen its surface to be observed.

    Constructors

    UnityHudElementViewTests()

    Declaration
    public UnityHudElementViewTests()

    Methods

    BuildPreview_AppliesTheSameAlignmentAsTheGamePath()

    The bug this whole property exists to close: the game path resolves its text from the theme's title template, the preview path from a bare object, and neither used to be told what alignment to use. BuildPreview(UnityHudElementDefinition, RectTransform, HudRenderResources) shares Construct with Build(UnityHudElementDefinition, RectTransform, HudRenderResources, Func<Type, object>), so asserting both land on the same value here is what keeps them from drifting apart again.

    Declaration
    public void BuildPreview_AppliesTheSameAlignmentAsTheGamePath()

    Build_FollowingTheTheme_TakesItsTypefaceAndColour()

    Declaration
    public void Build_FollowingTheTheme_TakesItsTypefaceAndColour()

    Build_LabelElement_IsItsCaption()

    Declaration
    public void Build_LabelElement_IsItsCaption()

    Build_LiteralCaption_CarriesNoLocalizationUpdater()

    Declaration
    public void Build_LiteralCaption_CarriesNoLocalizationUpdater()

    Build_ReadsItsValueFromTheResolvedService()

    Declaration
    public void Build_ReadsItsValueFromTheResolvedService()

    Build_SpriteElement_NeedsNoBindingAndReportsNoError()

    A sprite element is complete with nothing bound: it builds, draws, and reports no binding error, because an absent binding is its normal state rather than a fault.

    Declaration
    public void Build_SpriteElement_NeedsNoBindingAndReportsNoError()

    Build_StackWithSpentHidden_ConsumesFromTheGrowEnd()

    With spent slots hidden, every slot keeps its place — the graphic is switched off, the space is not. The row shrinking instead is the bug this guards against: the fitter re-wrapped the shorter row against the anchored edge, so the anchor, not the grow direction, decided which end was consumed.

    Declaration
    public void Build_StackWithSpentHidden_ConsumesFromTheGrowEnd()

    Build_StackWithSpentHidden_MirrorsWhenGrowingTheOtherWay()

    Declaration
    public void Build_StackWithSpentHidden_MirrorsWhenGrowingTheOtherWay()

    Build_Stack_PreserveAspectIsAuthoredNotHardcoded()

    Icon size means icon size: the sprite stretches to the authored rect unless the author opts into keeping its proportions. It used to be hardcoded on, which read as the vertical axis "shifting" the image while the horizontal one worked.

    Declaration
    public void Build_Stack_PreserveAspectIsAuthoredNotHardcoded()

    Build_StandingAlone_KeepsItsOwnColour()

    Declaration
    public void Build_StandingAlone_KeepsItsOwnColour()

    Build_TranslatingCaption_WiresTheLocalizationUpdater()

    The "Translate caption" toggle used to be consumed by nothing at runtime — a promise with no implementation. It now wires the same localization updater the modal uses; asserted synchronously so the component's own start pass, which needs a live localization service, never runs here.

    Declaration
    public void Build_TranslatingCaption_WiresTheLocalizationUpdater()

    Build_WithACaptionAndNoRepresentation_StillStylesTheCaption()

    An element with a caption and no representation used to be skipped by the styling pass, because it lived in the representation branch. This is the case that caught it.

    Declaration
    public void Build_WithACaptionAndNoRepresentation_StillStylesTheCaption()

    Build_WithANullResolver_DoesNotThrow()

    Declaration
    public void Build_WithANullResolver_DoesNotThrow()

    Build_WithAlignmentAuthored_AppliesItToEveryTextObject()

    Declaration
    public void Build_WithAlignmentAuthored_AppliesItToEveryTextObject()

    Build_WithNoAlignmentAuthored_DefaultsToCentered()

    Declaration
    public void Build_WithNoAlignmentAuthored_DefaultsToCentered()

    Build_WithNoResourcesAtAll_DoesNotThrow()

    Declaration
    public void Build_WithNoResourcesAtAll_DoesNotThrow()

    Build_WithNoServiceRegistered_ShowsTheFallbackRatherThanThrowing()

    Declaration
    public void Build_WithNoServiceRegistered_ShowsTheFallbackRatherThanThrowing()

    DeserializingAnElementSavedBeforeAlignmentExisted_DefaultsToCentered()

    Mirrors the useThemeSpacing/useThemeText precedent: a HUD saved before this field existed has no "textAlignment" key in its YAML at all, so Unity's deserializer never touches the field and it must keep its declared default — not read back as the enum's zero value by accident.

    Declaration
    public void DeserializingAnElementSavedBeforeAlignmentExisted_DefaultsToCentered()

    Dispose_DestroysTheElementAndIsSafeTwice()

    Declaration
    public void Dispose_DestroysTheElementAndIsSafeTwice()

    EnableValueChangeObservation_CalledAfterTickingHasAlreadyStarted_ReBaselinesInsteadOfReportingAStaleChange()

    Defensive guard for a caller that arms observation only after this view has already ticked once — not the production call sequence (the host always calls EnableValueChangeObservation before the first Tick), but the shared HudValueChangeDetector baseline is on the object, not per-caller, so a late enable must still never report the value's entire pre-arming history as one spurious first change.

    Declaration
    public void EnableValueChangeObservation_CalledAfterTickingHasAlreadyStarted_ReBaselinesInsteadOfReportingAStaleChange()

    MarkDirty_ForcesAReadRegardlessOfTheTrigger()

    Declaration
    public void MarkDirty_ForcesAReadRegardlessOfTheTrigger()

    RequestSignalAnimation_AfterBecomingVisibleAgain_FiresNormally()

    Declaration
    public void RequestSignalAnimation_AfterBecomingVisibleAgain_FiresNormally()

    RequestSignalAnimation_RespectsTheSameCooldownGateAsAValueTrigger()

    Declaration
    public void RequestSignalAnimation_RespectsTheSameCooldownGateAsAValueTrigger()

    RequestSignalAnimation_WhileHidden_DoesNotFire()

    Regression guard for the other half of the same review finding: the shared TryRequestAnimation chokepoint used to gate only on cooldown, so a signal landing on a hidden element still activated its layer — and activating a layer forces every GameObject it targets back active, defeating the hide.

    Declaration
    public void RequestSignalAnimation_WhileHidden_DoesNotFire()

    RequestValueAnimation_DoesNotFire_WhenTheDirectionDoesNotMatchTheTrigger()

    Declaration
    public void RequestValueAnimation_DoesNotFire_WhenTheDirectionDoesNotMatchTheTrigger()

    RequestValueAnimation_RespectsTheSameCooldownGateAsASignalTrigger()

    Declaration
    public void RequestValueAnimation_RespectsTheSameCooldownGateAsASignalTrigger()

    RequestValueAnimation_WhileHiddenWithPlaysWhileHidden_StillRespectsTheCooldownGate()

    PlaysWhileHidden bypasses only the hidden-element gate, not the cooldown gate — the two are independent checks in TryRequestAnimation, and this is the case that would catch them being accidentally coupled (e.g. an early-return that skipped the cooldown check whenever PlaysWhileHidden let the hidden check through).

    Declaration
    public void RequestValueAnimation_WhileHiddenWithPlaysWhileHidden_StillRespectsTheCooldownGate()

    RequestValueAnimation_WhileHidden_DoesNotFire_WhenPlaysWhileHiddenIsFalse()

    Regression guard for the "starts-hidden blink" bug: without PlaysWhileHidden, a value trigger landing on a hidden element must not activate a layer that forces its GameObject back active — the same rule RequestSignalAnimation_WhileHidden_DoesNotFire() already guards for the signal path, through the same TryRequestAnimation chokepoint.

    Declaration
    public void RequestValueAnimation_WhileHidden_DoesNotFire_WhenPlaysWhileHiddenIsFalse()

    RequestValueAnimation_WhileHidden_FiresWhenPlaysWhileHiddenIsTrue()

    The other half of the same guard: an entry authored with PlaysWhileHidden — the escape hatch for an activation/blink layer that owns the element's visibility for its own duration — must be let through the hidden-element gate rather than gated out with everything else.

    Declaration
    public void RequestValueAnimation_WhileHidden_FiresWhenPlaysWhileHiddenIsTrue()

    SetUp()

    Declaration
    public void SetUp()

    SetVisible_MirroringTheHostsBuildThenSinglePassShowForAStartsHiddenElement_NeverRaisesOnShow()

    Regression guard for the bug the fresh review caught: UnityHudHostComponent.ShowAllElements used to make every built element visible first and only afterwards re-hide whichever ones StartsHidden (or an explicit hide) remembered — which meant SetVisible(bool) was called with true, however briefly, on an element that was never meant to actually show, arming its ON_SHOW entries and (for a looping, non-repeating layer) leaving them stuck active. The fixed host computes each element's final visibility up front and calls SetVisible exactly once per element — for a hidden one, always with false. This mirrors that exact sequence at the view level, without needing a live host or animator: BuildElements' own immediate hide, then the single, already-correct SetVisible(false) the fixed ShowAllElements pass makes.

    Declaration
    public void SetVisible_MirroringTheHostsBuildThenSinglePassShowForAStartsHiddenElement_NeverRaisesOnShow()

    SetVisible_TogglesTheElement()

    Declaration
    public void SetVisible_TogglesTheElement()

    SetVisible_True_AfterAGenuineHide_RaisesOnShowAgain()

    Declaration
    public void SetVisible_True_AfterAGenuineHide_RaisesOnShowAgain()

    SetVisible_True_RaisesAnOnShowEntry()

    Declaration
    public void SetVisible_True_RaisesAnOnShowEntry()

    SetVisible_True_WhenAlreadyVisible_DoesNotReRaiseAnOnShowEntry()

    Declaration
    public void SetVisible_True_WhenAlreadyVisible_DoesNotReRaiseAnOnShowEntry()

    TearDown()

    Declaration
    public void TearDown()

    Tick_AfterEnablingValueChangeObservation_RaisesValueChanged_OnAnActualChange()

    Declaration
    public void Tick_AfterEnablingValueChangeObservation_RaisesValueChanged_OnAnActualChange()

    Tick_AfterTheCooldownElapses_FiresAgain()

    Declaration
    public void Tick_AfterTheCooldownElapses_FiresAgain()

    Tick_BeforeTheIntervalElapses_DoesNotReRead()

    Declaration
    public void Tick_BeforeTheIntervalElapses_DoesNotReRead()

    Tick_Gauge_MovingAboveEveryBand_RestoresTheAuthoredFillColour()

    The gauge's equivalent of Tick_MovingAboveEveryBand_RestoresTheStyledColour(): above every band the fill returns to whatever colour it was authored with, captured the first time a band ever overrides it.

    Declaration
    public void Tick_Gauge_MovingAboveEveryBand_RestoresTheAuthoredFillColour()

    Tick_Gauge_WithABandChangeButNoValueChange_RecolorsTheFillAndLeavesTheTrackAlone()

    The gauge's own equivalent of Tick_WithABandChangeButNoValueChange_RecolorsTheValueAndLeavesTheCaptionAlone(): a band change driven purely by capacity still has to repaint, because HudGaugeRenderer.Apply only ever moves the fill's anchors, never its colour — so a colour-only change would otherwise have nothing to invalidate it. The track is asserted separately because a threshold band must never reach it.

    Declaration
    public void Tick_Gauge_WithABandChangeButNoValueChange_RecolorsTheFillAndLeavesTheTrackAlone()

    Tick_MovingAboveEveryBand_RestoresTheStyledColour()

    Above every band the element keeps whatever colour it already had — the model has deliberately no base colour of its own — so leaving the band restores the sweep's own styled colour rather than some invented default.

    Declaration
    public void Tick_MovingAboveEveryBand_RestoresTheStyledColour()

    Tick_OnInterval_PicksUpAChangedServiceValue()

    Declaration
    public void Tick_OnInterval_PicksUpAChangedServiceValue()

    Tick_OnValueChange_FiresForEitherDirection()

    Declaration
    public void Tick_OnValueChange_FiresForEitherDirection()

    Tick_OnValueIncrease_DoesNotFire_WhenTheValueDecreases()

    Declaration
    public void Tick_OnValueIncrease_DoesNotFire_WhenTheValueDecreases()

    Tick_OnValueIncrease_RaisesAnimationRequested_ForAMatchingEntry()

    Declaration
    public void Tick_OnValueIncrease_RaisesAnimationRequested_ForAMatchingEntry()

    Tick_ReShowingAHiddenElement_DoesNotReplayTheAccumulatedChangeAsAnAnimationTrigger()

    Regression guard for the bug the design explicitly calls out: a value watcher must re-baseline silently while its element sits hidden, exactly like change feedback already does, or re-showing the element replays the accumulated hidden-time delta as a spurious ValueChanged.

    Declaration
    public void Tick_ReShowingAHiddenElement_DoesNotReplayTheAccumulatedChangeAsAnAnimationTrigger()

    Tick_ReShowingAHiddenElement_ReBaselinesValueChangedSilentlyAndRaisesNothing()

    Declaration
    public void Tick_ReShowingAHiddenElement_ReBaselinesValueChangedSilentlyAndRaisesNothing()

    Tick_Stack_BandChange_TintsFilledSlotsOnly_AndSurvivesACapacityRebuild()

    The repeated-sprite renderer's equivalent, on the stack representation it shares with segmented: a threshold band tints filled slots only, and the override has to keep landing on whichever slots are filled even after a capacity change forces HudRepeatedSpriteRenderer to grow its slot row — the case that breaks an override kept only as a one-shot paint rather than as state consulted on every repaint.

    Declaration
    public void Tick_Stack_BandChange_TintsFilledSlotsOnly_AndSurvivesACapacityRebuild()

    Tick_Stack_LeavingEveryBand_RestoresTheAuthoredFilledColour()

    Declaration
    public void Tick_Stack_LeavingEveryBand_RestoresTheAuthoredFilledColour()

    Tick_WithABandChangeButNoValueChange_RecolorsTheValueAndLeavesTheCaptionAlone()

    A band change driven purely by capacity — the value binding never moves — still has to repaint, because HudTextRepresentationRenderer's own change check is keyed on the rendered text, not the colour. This is the bug IHudValueColorable exists to close: a colour-only change routed through Apply alone would be swallowed by that same cache. The caption is asserted separately because the colour must land on the value's own text object only.

    Declaration
    public void Tick_WithABandChangeButNoValueChange_RecolorsTheValueAndLeavesTheCaptionAlone()

    Tick_WithNoActualChange_DoesNotRaiseValueChanged()

    Declaration
    public void Tick_WithNoActualChange_DoesNotRaiseValueChanged()

    Tick_WithinCooldown_DoesNotFireASecondTime()

    Declaration
    public void Tick_WithinCooldown_DoesNotFireASecondTime()

    Tick_WithoutEnablingValueChangeObservation_NeverRaisesValueChanged()

    Declaration
    public void Tick_WithoutEnablingValueChangeObservation_NeverRaisesValueChanged()
    In this article
    © 2026 Serenity. All Rights Reserved